linked-rolls 0.10.1 → 0.12.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 (42) hide show
  1. package/lib/Collation.d.ts +1 -1
  2. package/lib/Collation.js +53 -14
  3. package/lib/EditionView.d.ts +11 -0
  4. package/lib/EditionView.js +78 -82
  5. package/lib/Emulation.js +10 -16
  6. package/lib/Quantity.d.ts +4 -0
  7. package/lib/Quantity.js +3 -0
  8. package/lib/RollCopy.d.ts +43 -1
  9. package/lib/TrackerBar.d.ts +16 -1
  10. package/lib/TrackerBar.js +21 -0
  11. package/lib/Version.d.ts +5 -0
  12. package/lib/Version.js +4 -0
  13. package/lib/alignment.d.ts +8 -8
  14. package/lib/alignment.js +15 -15
  15. package/lib/constraints.js +4 -2
  16. package/lib/editionOps.d.ts +13 -5
  17. package/lib/editionOps.js +87 -37
  18. package/lib/importJsonLd.d.ts +3 -1
  19. package/lib/importJsonLd.js +22 -42
  20. package/lib/index.d.ts +3 -1
  21. package/lib/index.js +3 -1
  22. package/lib/migrate.js +34 -6
  23. package/lib/readers/spencerBar.d.ts +34 -0
  24. package/lib/readers/spencerBar.js +141 -0
  25. package/lib/readers/stanfordAton.d.ts +9 -1
  26. package/lib/readers/stanfordAton.js +30 -24
  27. package/lib/schema.json +90 -1
  28. package/lib/sorted.d.ts +6 -0
  29. package/lib/sorted.js +17 -0
  30. package/lib/spec/context.json +11 -1
  31. package/lib/systems/index.d.ts +6 -0
  32. package/lib/systems/index.js +10 -0
  33. package/lib/systems/welteLicensee/bar.d.ts +11 -0
  34. package/lib/systems/welteLicensee/bar.js +38 -0
  35. package/lib/systems/welteT100/bar.d.ts +2 -1
  36. package/lib/systems/welteT100/bar.js +4 -2
  37. package/lib/systems/welteT100/system.js +5 -4
  38. package/package.json +1 -1
  39. package/lib/readers/midiSpans.d.ts +0 -25
  40. package/lib/readers/midiSpans.js +0 -112
  41. package/lib/readers/spencerMidi.d.ts +0 -27
  42. package/lib/readers/spencerMidi.js +0 -60
@@ -25,9 +25,11 @@ const problemsIn = (version, perforations) => {
25
25
  .filter(p => p.pairedWith && idOf(p.pairedWith) === p.id)
26
26
  .map(p => report(p.id, 'paired-with-itself'));
27
27
  const pairs = pairsAmong(perforations);
28
- const pairsOf = (p) => pairs.filter(pair => pair.includes(p));
28
+ const pairsPerId = pairs
29
+ .flatMap(([one, other]) => one === other ? [one] : [one, other])
30
+ .reduce((counts, p) => counts.set(p.id, (counts.get(p.id) ?? 0) + 1), new Map());
29
31
  const inSeveralPairs = perforations
30
- .filter(p => pairsOf(p).length > 1)
32
+ .filter(p => (pairsPerId.get(p.id) ?? 0) > 1)
31
33
  .map(p => report(p.id, 'in-several-pairs'));
32
34
  const placedOnBothSides = pairs
33
35
  .filter(([one, other]) => placementsOf(one).length > 0 && placementsOf(other).length > 0)
@@ -4,8 +4,8 @@ import { Edition } from "./Edition";
4
4
  import { AnySymbol, PlacementRelation } from "./Symbol";
5
5
  import { CollationTolerance } from "./Collation";
6
6
  import { Edit } from "./Edit";
7
- import { PaperStretch, RollCopy, Shift } from "./RollCopy";
8
- import { AnyArgumentation, Certainty, ObjectAssumption } from "./Assumption";
7
+ import { RollCopy, ScaleReading, Shift } from "./RollCopy";
8
+ import { AnyArgumentation, Certainty } from "./Assumption";
9
9
  /**
10
10
  * A change to an edition, written onto an immer draft of it. One
11
11
  * operation is one undo step, so an operation that has to read the
@@ -18,9 +18,17 @@ export type EditionOp = (draft: Draft<Edition>) => void;
18
18
  * inserts every symbol the tracker bar reads on the copy.
19
19
  */
20
20
  export declare const createVersion: (siglum: string, copy: RollCopy) => EditionOp;
21
- /** Shifts and then stretches the copy's features into line with another copy's. */
22
- export declare const alignCopy: (copyId: string, shift: Shift, stretch: ObjectAssumption<PaperStretch>) => EditionOp;
23
- /** Puts the copy's features back where they were measured. */
21
+ /**
22
+ * Shifts and then scales the copy's features into line with another
23
+ * copy's, and puts the scale down to what the reading says: the paper,
24
+ * or the speed the copy was cut for.
25
+ */
26
+ export declare const alignCopy: (copyId: string, shift: Shift, scale: number, reading?: ScaleReading) => EditionOp;
27
+ /**
28
+ * Puts the copy's features back where they were measured. A paper
29
+ * stretch read off the alignment goes with it; a speed stated stays,
30
+ * being a fact about the copy.
31
+ */
24
32
  export declare const unalignCopy: (copyId: string) => EditionOp;
25
33
  /** The symbols of the versions that no other copy carries. */
26
34
  export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
package/lib/editionOps.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { current, isDraft } from "immer";
1
2
  import { v4 } from "uuid";
2
3
  import { getAt } from "./EditionView";
3
4
  import { isPerforation, placementRelations } from "./Symbol";
4
5
  import { collationsOf, defaultCollationTolerance } from "./Collation";
6
+ import { insertedBy } from "./Version";
5
7
  import { asSymbols } from "./RollCopy";
6
- import { applyShift, applyStretch, revertShift, revertStretch } from "./alignment";
8
+ import { applyShift, applyScale, revertShift, revertScale } from "./alignment";
7
9
  import { assignReference, idOf } from "./Assumption";
8
10
  import { distance, mm, subtract } from "./Quantity";
9
11
  const noChange = () => undefined;
@@ -17,23 +19,36 @@ const onVersion = (versionId, op) => draft => {
17
19
  if (version)
18
20
  op(version, draft);
19
21
  };
22
+ /**
23
+ * The state a draft stands at, as plain data. Reading a draft proxies
24
+ * everything it touches, so what is only read is read from this.
25
+ */
26
+ const stateOf = (draft) => isDraft(draft) ? current(draft) : draft;
20
27
  /** The items that do not match, or the very same array when none does, so that a draft stays untouched. */
21
28
  const without = (items, matches) => items.some(matches) ? items.filter(item => !matches(item)) : items;
29
+ /** The items each changed, or the very same array when the change left every one as it was. */
30
+ const mapped = (items, change) => {
31
+ const changed = items.map(change);
32
+ return changed.every((item, i) => item === items[i]) ? items : changed;
33
+ };
22
34
  const insertion = (symbol) => ({ type: 'edit', id: v4(), insert: [symbol] });
23
35
  const deletion = (symbolId) => ({ type: 'edit', id: v4(), delete: [symbolId] });
24
36
  const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
25
- const insertedIn = (versions) => versions.flatMap(version => version.edits).flatMap(edit => edit.insert ?? []);
26
- /** Runs the change over the version's edits and drops those it emptied, leaving edits that were empty before alone. */
27
- const editing = (version, change) => {
28
- const emptyBefore = new Set(version.edits.filter(isEmpty).map(edit => edit.id));
29
- version.edits.forEach(change);
30
- version.edits = without(version.edits, edit => isEmpty(edit) && !emptyBefore.has(edit.id));
37
+ const insertedIn = (versions) => versions.flatMap(insertedBy);
38
+ /** The edits with the change applied, less those it emptied; the very same array where it changed none. */
39
+ const edited = (edits, change) => {
40
+ const changed = mapped(edits, change);
41
+ return changed === edits ? edits : changed.filter((edit, i) => edit === edits[i] || !isEmpty(edit));
42
+ };
43
+ /** The edit without the symbols among its insertions, or the very same edit where it inserts none of them. */
44
+ const droppingInsertions = (symbolIds) => (edit) => {
45
+ const insert = edit.insert && without(edit.insert, symbol => symbolIds.has(symbol.id));
46
+ return insert === edit.insert ? edit : { ...edit, insert };
47
+ };
48
+ /** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
49
+ const dropInsertions = (version, symbolIds) => {
50
+ version.edits = edited(stateOf(version).edits, droppingInsertions(symbolIds));
31
51
  };
32
- /** Takes the symbols out of the version's own insertions. */
33
- const dropInsertions = (version, symbolIds) => editing(version, edit => {
34
- if (edit.insert)
35
- edit.insert = without(edit.insert, symbol => symbolIds.has(symbol.id));
36
- });
37
52
  /**
38
53
  * Puts the copy into the edition with a version of its own, which
39
54
  * inserts every symbol the tracker bar reads on the copy.
@@ -49,15 +64,37 @@ export const createVersion = (siglum, copy) => draft => {
49
64
  motivations: []
50
65
  });
51
66
  };
52
- /** Shifts and then stretches the copy's features into line with another copy's. */
53
- export const alignCopy = (copyId, shift, stretch) => onCopy(copyId, copy => {
67
+ const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
68
+ /** States what the scale is put down to, in place of an earlier reading. */
69
+ const readScale = (copy, reading) => {
70
+ if (reading.cause === 'paper') {
71
+ copy.conditions = [...copy.conditions.filter(condition => !isPaperStretch(condition)), reading.condition];
72
+ return;
73
+ }
74
+ if (!copy.production)
75
+ copy.production = {};
76
+ copy.production.speed = reading.speed;
77
+ };
78
+ /**
79
+ * Shifts and then scales the copy's features into line with another
80
+ * copy's, and puts the scale down to what the reading says: the paper,
81
+ * or the speed the copy was cut for.
82
+ */
83
+ export const alignCopy = (copyId, shift, scale, reading) => onCopy(copyId, copy => {
54
84
  applyShift(shift, copy);
55
- applyStretch(stretch, copy);
85
+ applyScale(scale, copy);
86
+ if (reading)
87
+ readScale(copy, reading);
56
88
  });
57
- /** Puts the copy's features back where they were measured. */
89
+ /**
90
+ * Puts the copy's features back where they were measured. A paper
91
+ * stretch read off the alignment goes with it; a speed stated stays,
92
+ * being a fact about the copy.
93
+ */
58
94
  export const unalignCopy = (copyId) => onCopy(copyId, copy => {
59
- revertStretch(copy);
95
+ revertScale(copy);
60
96
  revertShift(copy);
97
+ copy.conditions = without(copy.conditions, isPaperStretch);
61
98
  });
62
99
  const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
63
100
  /**
@@ -72,25 +109,34 @@ export const symbolsCarriedOnlyBy = (edition, copyId) => {
72
109
  return copy ? insertedIn(edition.versions).filter(carriedOnlyOn(featureIdsOf(copy))) : [];
73
110
  };
74
111
  const references = [...placementRelations, 'pairedWith'];
75
- const forgetPerforations = (perforation, dropped) => references
76
- .filter(relation => {
77
- const reference = perforation[relation];
78
- return reference && dropped.has(idOf(reference));
79
- })
80
- .forEach(relation => { delete perforation[relation]; });
81
- const forgetCarriers = (symbol, features, dropped) => {
82
- symbol.carriers = without(symbol.carriers, carrier => features.has(idOf(carrier)));
83
- if (isPerforation(symbol))
84
- forgetPerforations(symbol, dropped);
112
+ /** The perforation without its references to the dropped symbols, or the very same one where it makes none. */
113
+ const forgettingReferences = (perforation, dropped) => {
114
+ const stale = references.filter(relation => {
115
+ const reference = perforation[relation];
116
+ return reference !== undefined && dropped.has(idOf(reference));
117
+ });
118
+ if (stale.length === 0)
119
+ return perforation;
120
+ const kept = { ...perforation };
121
+ stale.forEach(relation => { delete kept[relation]; });
122
+ return kept;
85
123
  };
86
- const forgetFeaturesInEdit = (edit, features, dropped) => {
87
- if (edit.insert) {
88
- edit.insert = without(edit.insert, symbol => dropped.has(symbol.id));
89
- edit.insert.forEach(symbol => forgetCarriers(symbol, features, dropped));
90
- }
91
- if (edit.delete) {
92
- edit.delete = without(edit.delete, id => dropped.has(id));
93
- }
124
+ /** The symbol without the features among its carriers, and without references to what went with them. */
125
+ const forgettingCarriers = (features, dropped) => (symbol) => {
126
+ const carriers = without(symbol.carriers, carrier => features.has(idOf(carrier)));
127
+ const relieved = carriers === symbol.carriers ? symbol : { ...symbol, carriers };
128
+ return isPerforation(relieved) ? forgettingReferences(relieved, dropped) : relieved;
129
+ };
130
+ /** The edit without the dropped symbols and the features, or the very same edit where it had none of them. */
131
+ const forgettingFeatures = (features, dropped) => {
132
+ const forgetCarriers = forgettingCarriers(features, dropped);
133
+ return (edit) => {
134
+ const insert = edit.insert && mapped(without(edit.insert, symbol => dropped.has(symbol.id)), forgetCarriers);
135
+ const deleted = edit.delete && without(edit.delete, id => dropped.has(id));
136
+ if (insert === edit.insert && deleted === edit.delete)
137
+ return edit;
138
+ return { ...edit, ...(insert && { insert }), ...(deleted && { delete: deleted }) };
139
+ };
94
140
  };
95
141
  /**
96
142
  * Strikes the features from the versions: their carriers go, a symbol
@@ -98,8 +144,12 @@ const forgetFeaturesInEdit = (edit, features, dropped) => {
98
144
  * reference the versions made to such a symbol.
99
145
  */
100
146
  const forgetFeatures = (draft, features) => {
101
- const dropped = new Set(insertedIn(draft.versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
102
- draft.versions.forEach(version => editing(version, edit => forgetFeaturesInEdit(edit, features, dropped)));
147
+ const versions = stateOf(draft.versions);
148
+ const dropped = new Set(insertedIn(versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
149
+ const forget = forgettingFeatures(features, dropped);
150
+ draft.versions.forEach((version, i) => {
151
+ version.edits = edited(versions[i].edits, forget);
152
+ });
103
153
  };
104
154
  /** Takes the features off the copy, and out of the versions with what only they carried. */
105
155
  export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
@@ -1,3 +1,5 @@
1
1
  import { Edition } from "./Edition";
2
2
  export declare const importDate: (str: string) => Date;
3
- export declare const importJsonLd: (json: any) => Edition;
3
+ type Json = any;
4
+ export declare const importJsonLd: (json: Json) => Edition;
5
+ export {};
@@ -10,49 +10,29 @@ export const importDate = (str) => {
10
10
  }
11
11
  return new Date(y, m - 1, d);
12
12
  };
13
+ /** A value as the edition holds it: a date read, an entity converted, anything else as it stands. */
14
+ const fromJsonLdValue = (value) => {
15
+ if (typeof value === 'string')
16
+ return isDate(value) ? importDate(value) : value;
17
+ if (Array.isArray(value))
18
+ return value.map(fromJsonLdValue);
19
+ if (value !== null && typeof value === 'object')
20
+ return fromJsonLdEntity(value);
21
+ return value;
22
+ };
23
+ /**
24
+ * An entity with its keywords read as plain keys. The input is left as
25
+ * it is. The `@type` of a value object names the datatype of the value,
26
+ * not a class, and is dropped.
27
+ */
13
28
  const fromJsonLdEntity = (json) => {
14
- if (typeof json !== 'object') {
15
- return json;
16
- }
17
- let result = json;
18
- if ('@value' in json) {
19
- // the datatype of a value object, not a class
20
- delete result['@type'];
21
- }
22
- else if ('@type' in json) {
23
- result['type'] = json['@type'];
24
- delete result['@type'];
25
- }
26
- for (const [key, value] of Object.entries(json)) {
27
- if (key === '@id') {
28
- // console.log("deleting @id", value);
29
- result['id'] = value;
30
- delete result['@id'];
31
- }
32
- else if (typeof value === 'string' && isDate(value)) {
33
- result[key] = importDate(value);
34
- }
35
- else if (Array.isArray(value)) {
36
- result[key] = value.map(v => {
37
- if (typeof v === 'string') {
38
- if (isDate(v)) {
39
- return importDate(v);
40
- }
41
- return v;
42
- }
43
- else {
44
- return fromJsonLdEntity(v);
45
- }
46
- });
47
- }
48
- else if (typeof value === 'object') {
49
- result[key] = fromJsonLdEntity(value);
50
- }
51
- else {
52
- result[key] = value;
53
- }
54
- }
55
- return result;
29
+ const { '@type': type, '@id': id, ...rest } = json;
30
+ const entity = Object.fromEntries(Object.entries(rest).map(([key, value]) => [key, fromJsonLdValue(value)]));
31
+ if (type !== undefined && !('@value' in json))
32
+ entity.type = type;
33
+ if (id !== undefined)
34
+ entity.id = id;
35
+ return entity;
56
36
  };
57
37
  // The export prefixes copy identifiers with `copy/`; this is its inverse.
58
38
  const withPlainCopyIds = (json) => ({
package/lib/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export * from './Collation';
11
11
  export * from './TrackCalibration';
12
12
  export * from './TrackerBar';
13
13
  export * from './systems/welteT100/bar';
14
+ export * from './systems/welteLicensee/bar';
15
+ export * from './systems';
14
16
  export * from './ReproducingSystem';
15
17
  export * from './RollCopy';
16
18
  export * from './alignment';
@@ -24,4 +26,4 @@ export * from './context';
24
26
  export * from './asJsonLd';
25
27
  export * from './importJsonLd';
26
28
  export { readFromStanfordAton, type StanfordAtonOptions } from './readers/stanfordAton';
27
- export { readFromSpencerMIDI, atConstantSpeed, spencerTrackOf, SPENCER_FEET_PER_MINUTE } from './readers/spencerMidi';
29
+ export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH, type SpencerBarOptions } from './readers/spencerBar';
package/lib/index.js CHANGED
@@ -11,6 +11,8 @@ export * from './Collation';
11
11
  export * from './TrackCalibration';
12
12
  export * from './TrackerBar';
13
13
  export * from './systems/welteT100/bar';
14
+ export * from './systems/welteLicensee/bar';
15
+ export * from './systems';
14
16
  export * from './ReproducingSystem';
15
17
  export * from './RollCopy';
16
18
  export * from './alignment';
@@ -24,4 +26,4 @@ export * from './context';
24
26
  export * from './asJsonLd';
25
27
  export * from './importJsonLd';
26
28
  export { readFromStanfordAton } from './readers/stanfordAton';
27
- export { readFromSpencerMIDI, atConstantSpeed, spencerTrackOf, SPENCER_FEET_PER_MINUTE } from './readers/spencerMidi';
29
+ export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH } from './readers/spencerBar';
package/lib/migrate.js CHANGED
@@ -12,7 +12,9 @@ const renamedKeys = {
12
12
  };
13
13
  const referenceKeys = ['alignedWith', 'pairedWith', 'basedOn'];
14
14
  const named = (name) => ({ name, sameAs: [] });
15
- const withRenamedKeys = (node) => Object.fromEntries(Object.entries(node).map(([key, value]) => [renamedKeys[key] ?? key, value]));
15
+ const withRenamedKeys = (node) => Object.keys(node).some(key => Object.hasOwn(renamedKeys, key))
16
+ ? Object.fromEntries(Object.entries(node).map(([key, value]) => [renamedKeys[key] ?? key, value]))
17
+ : node;
16
18
  const withTypology = (node) => {
17
19
  if (versionTypeValues.has(node['@type'])) {
18
20
  return { ...node, '@type': 'Version', versionType: node['@type'] };
@@ -47,17 +49,43 @@ const withProductionNodes = (node) => {
47
49
  ...rest,
48
50
  ...(typeof company === 'string' ? (company && { company: named(company) }) : { company }),
49
51
  ...(typeof paper === 'string' ? (paper && { paper: named(paper) }) : { paper }),
52
+ // a 0.1 file named the roll's system here as text; a copy's own system is a node
53
+ ...(system && typeof system === 'object' && { system })
50
54
  }
51
55
  };
52
56
  };
53
- const migrateNode = (node) => [withRenamedKeys, withTypology, withReferences, withKeeper, withProductionNodes]
57
+ const isPaperStretch = (condition) => condition?.conditionType === 'paper-stretch' || condition?.['@type'] === 'paper-stretch';
58
+ /**
59
+ * The scale of an aligned copy used to be recorded only as the factor
60
+ * of its paper-stretch condition. It is the alignment's own number
61
+ * now, and the condition stays as the reading of it. The conditions
62
+ * are still in their old shape here, since a node is migrated before
63
+ * its children are.
64
+ */
65
+ const withScale = (node) => {
66
+ if (!Array.isArray(node.ops) || !node.ops.includes('stretched') || node.measurements?.scale !== undefined)
67
+ return node;
68
+ const factor = (node.conditions ?? []).find(isPaperStretch)?.factor;
69
+ return factor === undefined ? node : { ...node, measurements: { ...node.measurements, scale: factor } };
70
+ };
71
+ const migrateNode = (node) => [withRenamedKeys, withTypology, withReferences, withKeeper, withProductionNodes, withScale]
54
72
  .reduce((result, step) => step(result), node);
73
+ /** The items each walked, or the very same list where the walk changed none. */
74
+ const walked = (items) => {
75
+ const result = items.map(walk);
76
+ return result.every((item, i) => item === items[i]) ? items : result;
77
+ };
78
+ /** The node with each child walked, or the very same node where the walk changed none. */
79
+ const withWalkedChildren = (node) => {
80
+ const entries = Object.entries(node);
81
+ const result = entries.map(([key, child]) => [key, walk(child)]);
82
+ return result.every(([, child], i) => child === entries[i][1]) ? node : Object.fromEntries(result);
83
+ };
55
84
  const walk = (value) => {
56
85
  if (Array.isArray(value))
57
- return value.map(walk);
58
- if (value && typeof value === 'object') {
59
- return Object.fromEntries(Object.entries(migrateNode(value)).map(([key, child]) => [key, walk(child)]));
60
- }
86
+ return walked(value);
87
+ if (value && typeof value === 'object')
88
+ return withWalkedChildren(migrateNode(value));
61
89
  return value;
62
90
  };
63
91
  /**
@@ -0,0 +1,34 @@
1
+ import { PaperSpeed, RollCopy } from "../RollCopy";
2
+ import { TrackerBar } from "../TrackerBar";
3
+ /**
4
+ * Rows of the image on an inch of paper. The player reads eight rows a
5
+ * second per unit of roll tempo (tempo 80 comes with a sample rate of
6
+ * 640 Hz), and the tempo counts tenths of a foot per minute, which puts
7
+ * 400 rows on an inch. This is the file's own calibration; whether the
8
+ * scanner kept it is for an alignment with other copies to tell.
9
+ */
10
+ export declare const SPENCER_ROWS_PER_INCH = 400;
11
+ export interface SpencerBarOptions {
12
+ /** Rows of the image on an inch of paper. */
13
+ rowsPerInch?: number;
14
+ /**
15
+ * The bar the file numbers its positions by, which is the bar of
16
+ * the roll it was scanned from. His Welte files are Licensee rolls.
17
+ */
18
+ system?: TrackerBar;
19
+ /** The edition's bar, onto which the positions are put. */
20
+ bar?: TrackerBar;
21
+ }
22
+ /**
23
+ * Reads the copy onto the edition's bar. A hole on a position the
24
+ * edition's bar does not read is left out, as the bar would leave it.
25
+ */
26
+ export declare function readFromSpencerBar(buffer: ArrayBuffer, { rowsPerInch, system, bar }?: SpencerBarOptions): RollCopy;
27
+ /**
28
+ * The `.ann` file beside a `.bar` holds the player's settings for the
29
+ * roll as lines of "/key: value": title, composer, pianist, roll
30
+ * number and class, and the tempo the roll is played at.
31
+ */
32
+ export declare const readSpencerAnn: (text: string) => ReadonlyMap<string, string>;
33
+ /** The paper speed a `.ann` states through its roll tempo, where it states one. */
34
+ export declare const paperSpeedOfSpencerAnn: (ann: ReadonlyMap<string, string>) => PaperSpeed | undefined;
@@ -0,0 +1,141 @@
1
+ import { v4 } from "uuid";
2
+ import { systemOf, translationBetween } from "../TrackerBar";
3
+ import { welteT100 } from "../systems/welteT100/bar";
4
+ import { welteLicensee } from "../systems/welteLicensee/bar";
5
+ import { feetPerMinute, inMillimeters, px, track } from "../Quantity";
6
+ /**
7
+ * Spencer Chase's e-roll file (`.bar`, "eRoll Tracker Bar Image") holds
8
+ * a roll as a list of events: a distance in rows of the scanned image,
9
+ * then the tracker bar position whose hole begins or ends on that row.
10
+ *
11
+ * Layout, as read off W225E.bar: four bytes (00 01 00 00), the tag F4
12
+ * and a null-terminated text ("/oldtrackname:..."), then the events,
13
+ * each an unsigned LEB128 distance and one position byte. A position
14
+ * byte of FF closes the list. A hole ends when its position turns up
15
+ * a second time.
16
+ */
17
+ const TEXT_AT = 4;
18
+ const TEXT_TAG = 0xF4;
19
+ const END_OF_EVENTS = 0xFF;
20
+ /**
21
+ * Rows of the image on an inch of paper. The player reads eight rows a
22
+ * second per unit of roll tempo (tempo 80 comes with a sample rate of
23
+ * 640 Hz), and the tempo counts tenths of a foot per minute, which puts
24
+ * 400 rows on an inch. This is the file's own calibration; whether the
25
+ * scanner kept it is for an alignment with other copies to tell.
26
+ */
27
+ export const SPENCER_ROWS_PER_INCH = 400;
28
+ const byteAt = (bytes, at) => {
29
+ const byte = bytes[at];
30
+ if (byte === undefined)
31
+ throw new Error(`Spencer .bar file ends early at byte ${at}`);
32
+ return byte;
33
+ };
34
+ const leb128 = (bytes, at) => {
35
+ let value = 0;
36
+ let next = at;
37
+ for (let weight = 1;; weight *= 128) {
38
+ const byte = byteAt(bytes, next++);
39
+ value += (byte & 0x7F) * weight;
40
+ if (!(byte & 0x80))
41
+ return { value, next };
42
+ }
43
+ };
44
+ const endOfText = (bytes, from) => {
45
+ const end = bytes.indexOf(0, from);
46
+ if (end < 0)
47
+ throw new Error('Spencer .bar file: the text is not terminated');
48
+ return end + 1;
49
+ };
50
+ function* eventsIn(bytes, from) {
51
+ let row = 0;
52
+ let at = from;
53
+ while (true) {
54
+ const distance = leb128(bytes, at);
55
+ const position = byteAt(bytes, distance.next);
56
+ if (position === END_OF_EVENTS)
57
+ return;
58
+ row += distance.value;
59
+ yield { row, position };
60
+ at = distance.next + 1;
61
+ }
62
+ }
63
+ /** Pairs the events of each position into holes: the first opens one, the next closes it. */
64
+ const holesOf = (events) => {
65
+ const open = new Map();
66
+ const holes = [];
67
+ for (const { row, position } of events) {
68
+ const from = open.get(position);
69
+ if (from === undefined) {
70
+ open.set(position, row);
71
+ }
72
+ else {
73
+ holes.push({ position, from, to: row });
74
+ open.delete(position);
75
+ }
76
+ }
77
+ if (open.size > 0) {
78
+ throw new Error(`Spencer .bar file: holes on positions ${[...open.keys()].join(', ')} never end`);
79
+ }
80
+ return holes.sort((a, b) => a.from - b.from);
81
+ };
82
+ /**
83
+ * Reads the copy onto the edition's bar. A hole on a position the
84
+ * edition's bar does not read is left out, as the bar would leave it.
85
+ */
86
+ export function readFromSpencerBar(buffer, { rowsPerInch = SPENCER_ROWS_PER_INCH, system = welteLicensee, bar = welteT100 } = {}) {
87
+ const bytes = new Uint8Array(buffer);
88
+ if (byteAt(bytes, TEXT_AT) !== TEXT_TAG) {
89
+ throw new Error('Not a Spencer .bar file: no text after the header');
90
+ }
91
+ const placeOf = (row) => inMillimeters(px(row), rowsPerInch);
92
+ const onBar = translationBetween(system, bar);
93
+ const features = holesOf(eventsIn(bytes, endOfText(bytes, TEXT_AT + 1)))
94
+ .flatMap((hole) => {
95
+ const position = onBar(track(hole.position));
96
+ if (position === undefined)
97
+ return [];
98
+ return [{
99
+ type: 'Hole',
100
+ id: v4(),
101
+ vertical: {
102
+ from: position,
103
+ unit: 'track'
104
+ },
105
+ horizontal: {
106
+ unit: 'mm',
107
+ from: placeOf(hole.from),
108
+ to: placeOf(hole.to)
109
+ }
110
+ }];
111
+ });
112
+ return {
113
+ type: 'RollCopy',
114
+ id: v4(),
115
+ ops: [],
116
+ conditions: [],
117
+ keeper: { name: '', sameAs: [] },
118
+ measurements: {},
119
+ production: { system: systemOf(system) },
120
+ modifications: [],
121
+ features
122
+ };
123
+ }
124
+ /**
125
+ * The `.ann` file beside a `.bar` holds the player's settings for the
126
+ * roll as lines of "/key: value": title, composer, pianist, roll
127
+ * number and class, and the tempo the roll is played at.
128
+ */
129
+ export const readSpencerAnn = (text) => new Map(text.split(/\r?\n/)
130
+ .map(line => line.match(/^\/(\w+):\s*(.*?)\s*$/))
131
+ .filter((match) => match !== null)
132
+ .map(([, key, value]) => [key, value]));
133
+ /** A roll tempo counts tenths of a foot per minute: tempo 83 runs the roll at 8.3 feet a minute. */
134
+ const TEMPO_PER_FOOT_PER_MINUTE = 10;
135
+ /** The paper speed a `.ann` states through its roll tempo, where it states one. */
136
+ export const paperSpeedOfSpencerAnn = (ann) => {
137
+ const tempo = parseFloat(ann.get('roll_tempo') ?? '');
138
+ if (isNaN(tempo) || tempo <= 0)
139
+ return undefined;
140
+ return { value: feetPerMinute(tempo / TEMPO_PER_FOOT_PER_MINUTE), unit: 'ft/min' };
141
+ };
@@ -8,7 +8,15 @@ export interface StanfordAtonOptions {
8
8
  * perforation on the bar's rewind track.
9
9
  */
10
10
  trackShift?: Track;
11
+ /** The edition's bar, onto which the holes are put. */
11
12
  bar?: TrackerBar;
13
+ /**
14
+ * The bar the scanned roll was cut for, where it is not the
15
+ * edition's. The scan is calibrated on it, and the holes are then
16
+ * put onto the edition's bar; one on a position that bar does not
17
+ * read is left out.
18
+ */
19
+ system?: TrackerBar;
12
20
  /**
13
21
  * Where the scan the analysis was made from can be seen. Stanford's
14
22
  * files name their scan by DRUID, so this is only needed for an
@@ -16,4 +24,4 @@ export interface StanfordAtonOptions {
16
24
  */
17
25
  scan?: string;
18
26
  }
19
- export declare function readFromStanfordAton(atonString: string, { trackShift, bar, scan }?: StanfordAtonOptions): RollCopy;
27
+ export declare function readFromStanfordAton(atonString: string, { trackShift, bar, system, scan }?: StanfordAtonOptions): RollCopy;