linked-rolls 0.11.0 → 0.13.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.
package/lib/Quantity.d.ts CHANGED
@@ -58,4 +58,8 @@ export declare const mean: <U extends Unit>(values: readonly Quantity<U>[]) => Q
58
58
  export declare const inMillimeters: (place: Pixels, dpi: number) => Millimeters;
59
59
  export declare const inCentimeters: (length: Millimeters) => Centimeters;
60
60
  export declare const inSeconds: (time: Milliseconds) => Seconds;
61
+ /** A speed as a record states it, in feet or metres per minute. */
62
+ export type SpeedMeasure = Measure<'ft/min'> | Measure<'m/min'>;
63
+ /** A speed in metres per minute, whichever unit it was stated in. */
64
+ export declare const inMetersPerMinute: (speed: SpeedMeasure) => MetersPerMinute;
61
65
  export {};
package/lib/Quantity.js CHANGED
@@ -23,3 +23,6 @@ const MM_PER_INCH = 25.4;
23
23
  export const inMillimeters = (place, dpi) => mm(place / dpi * MM_PER_INCH);
24
24
  export const inCentimeters = (length) => cm(length / 10);
25
25
  export const inSeconds = (time) => seconds(time / 1000);
26
+ const METERS_PER_FOOT = 0.3048;
27
+ /** A speed in metres per minute, whichever unit it was stated in. */
28
+ export const inMetersPerMinute = (speed) => speed.unit === 'm/min' ? speed.value : metersPerMinute(speed.value * METERS_PER_FOOT);
package/lib/RollCopy.d.ts CHANGED
@@ -51,9 +51,28 @@ export interface Margins<U extends 'px' | 'mm'> {
51
51
  bass: Quantity<U>;
52
52
  unit: U;
53
53
  }
54
+ /**
55
+ * A paper speed, as a roll's label, its catalogue or its format
56
+ * states it.
57
+ * @see crm:E54 Dimension
58
+ */
59
+ export type PaperSpeed = Measure<'ft/min'> | Measure<'m/min'>;
60
+ /**
61
+ * What the scale an alignment found is put down to: the paper of the
62
+ * copy having stretched or shrunk, or the copy having been cut for
63
+ * another paper speed than the roll it is aligned with.
64
+ */
65
+ export type ScaleReading = {
66
+ cause: 'paper';
67
+ condition: ObjectAssumption<PaperStretch>;
68
+ } | {
69
+ cause: 'speed';
70
+ speed: ObjectAssumption<PaperSpeed>;
71
+ };
54
72
  /**
55
73
  * Describes the production of a roll copy: the manufacturer,
56
- * the paper used, and the date.
74
+ * the paper used, the date, and the system and paper speed the
75
+ * copy was cut for.
57
76
  * @see lrmoo:F32 Item Production Event
58
77
  */
59
78
  export interface ProductionEvent {
@@ -73,6 +92,22 @@ export interface ProductionEvent {
73
92
  * @see dcterms:date
74
93
  */
75
94
  date?: DateAssignment;
95
+ /**
96
+ * The reproducing system the copy was cut for. Left out, it is
97
+ * the roll's own; a Licensee re-cut of a T-100 roll names the
98
+ * Licensee here. A system the type vocabulary knows carries the
99
+ * IRI of its concept as `id`.
100
+ * @see crm:P32 used general technique
101
+ */
102
+ system?: Concept;
103
+ /**
104
+ * The paper speed the copy was cut for. A copy cut from the same
105
+ * master for another speed comes out longer or shorter than the
106
+ * roll it is aligned with by the ratio of the speeds, which is
107
+ * what the alignment then measures.
108
+ * @see reo:paperSpeed
109
+ */
110
+ speed?: ObjectAssumption<PaperSpeed>;
76
111
  }
77
112
  /**
78
113
  * This type denotes identifiable activities that modified
@@ -179,6 +214,13 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
179
214
  * Not exported to RDF.
180
215
  */
181
216
  shift: Shift;
217
+ /**
218
+ * The factor this copy's features were scaled by to align them
219
+ * with the others. What it is put down to is stated apart: a
220
+ * paper-stretch condition, or the speed the copy was cut for.
221
+ * Not exported to RDF.
222
+ */
223
+ scale: number;
182
224
  /**
183
225
  * Relates this copy's scan to the tracker bar: how the scanning
184
226
  * software's hole numbering was shifted onto the bar, and where
@@ -1,6 +1,6 @@
1
1
  import type { Concept } from "./Agent";
2
2
  import { Expression, Note } from "./Symbol";
3
- import { Millimeters, Track } from "./Quantity";
3
+ import { Millimeters, SpeedMeasure, Track } from "./Quantity";
4
4
  /**
5
5
  * What a tracker bar position does: sound a note, or operate one of
6
6
  * the expression valves on the bass or the treble side.
@@ -50,6 +50,12 @@ export interface TrackerBar {
50
50
  * a scan against the bar.
51
51
  */
52
52
  readonly rewindTrack: Track;
53
+ /**
54
+ * The paper speed the system runs its rolls at, where the
55
+ * literature states one. A system whose rolls each carry a tempo
56
+ * of their own, as the Licensee's do, states none.
57
+ */
58
+ readonly paperSpeed?: SpeedMeasure;
53
59
  /** `undefined` for a position the bar does not read. */
54
60
  meaningOf(position: Track): TrackMeaning | undefined;
55
61
  /** `undefined` for a position the bar does not read. */
@@ -77,5 +83,14 @@ export interface TrackerBarSpec {
77
83
  };
78
84
  /** Every position outside the note block, keyed by track. */
79
85
  expressions: ReadonlyMap<number, string>;
86
+ /** The speed the system runs its rolls at, where the literature states one. */
87
+ paperSpeed?: SpeedMeasure;
80
88
  }
81
89
  export declare const describeTrackerBar: (spec: TrackerBarSpec) => TrackerBar;
90
+ /**
91
+ * Puts a position of one bar onto the position of another that reads
92
+ * the same thing, or nowhere when the other bar does not read it. This
93
+ * is how a copy cut for one system takes its place in an edition of
94
+ * another, as a Licensee re-cut does in an edition of a T-100 roll.
95
+ */
96
+ export declare const translationBetween: (from: TrackerBar, to: TrackerBar) => (position: Track) => Track | undefined;
package/lib/TrackerBar.js CHANGED
@@ -41,7 +41,28 @@ export const describeTrackerBar = (spec) => {
41
41
  areas,
42
42
  expressionTypes: [...new Set(spec.expressions.values())],
43
43
  rewindTrack: track(rewind),
44
+ ...(spec.paperSpeed && { paperSpeed: spec.paperSpeed }),
44
45
  meaningOf,
45
46
  roleOf
46
47
  };
47
48
  };
49
+ const keyOf = (meaning) => meaning.type === 'note' ? `note ${meaning.pitch}` : `${meaning.scope} ${meaning.expressionType}`;
50
+ const positionsOf = (bar) => Array.from({ length: bar.trackCount }, (_, i) => track(i + 1));
51
+ /**
52
+ * Puts a position of one bar onto the position of another that reads
53
+ * the same thing, or nowhere when the other bar does not read it. This
54
+ * is how a copy cut for one system takes its place in an edition of
55
+ * another, as a Licensee re-cut does in an edition of a T-100 roll.
56
+ */
57
+ export const translationBetween = (from, to) => {
58
+ const positions = new Map();
59
+ positionsOf(to).forEach(position => {
60
+ const meaning = to.meaningOf(position);
61
+ if (meaning)
62
+ positions.set(keyOf(meaning), position);
63
+ });
64
+ return position => {
65
+ const meaning = from.meaningOf(position);
66
+ return meaning ? positions.get(keyOf(meaning)) : undefined;
67
+ };
68
+ };
@@ -1,22 +1,35 @@
1
- import { ObjectAssumption } from "./Assumption";
2
1
  import { AnyFeature } from "./Feature";
3
- import { PaperStretch, RollCopy, Shift } from "./RollCopy";
2
+ import { RollCopy, Shift } from "./RollCopy";
4
3
  import { TrackerBar } from "./TrackerBar";
5
4
  import { Millimeters } from "./Quantity";
6
5
  export declare const applyShift: (shift: Shift, copy: RollCopy) => void;
7
- export declare const applyStretch: (paperStretch: ObjectAssumption<PaperStretch>, copy: RollCopy) => void;
6
+ /** Scales the copy's features away from the beginning of the roll, and records the factor. */
7
+ export declare const applyScale: (factor: number, copy: RollCopy) => void;
8
8
  /** Takes the shift off the copy's features again, as far as one was applied. */
9
9
  export declare const revertShift: (copy: RollCopy) => void;
10
- /** Takes the stretch off the copy's features again, as far as one was applied. */
11
- export declare const revertStretch: (copy: RollCopy) => void;
12
- type AlignmentResult = {
13
- /** Applied before the stretch. */
10
+ /** Takes the scale off the copy's features again, as far as one was applied. */
11
+ export declare const revertScale: (copy: RollCopy) => void;
12
+ /**
13
+ * How a copy's places are carried onto another copy's:
14
+ * `x_other = (x + shift) · scale`.
15
+ */
16
+ export interface AlignmentResult {
17
+ /** Applied before the scale. */
14
18
  shift: Millimeters;
15
- stretch: number;
16
- };
19
+ scale: number;
20
+ /** The notes of the copy that found their counterpart on the other. */
21
+ matched: number;
22
+ /** How far the counterparts still lie apart, as a root mean square in the other copy's millimetres. */
23
+ residual: Millimeters;
24
+ }
17
25
  /**
18
- * Align two rolls by computing independent linear fits of each roll's note-onset positions
19
- * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
26
+ * Finds the shift and scale that carry the places of `rollA` onto those
27
+ * of `rollB`, reading both through the bar. Runs of pitches that occur
28
+ * once on each roll anchor a first line; then every note is paired with
29
+ * its nearest counterpart of the same pitch and the line refitted, the
30
+ * window closing each time. A copy cut for another paper speed, a scan
31
+ * with a calibration pattern, and holes the other copy lacks are all
32
+ * within reach of that. Nothing is found where the rolls share no run of
33
+ * pitches, as between two different pieces.
20
34
  */
21
- export declare function alignFeatures(rollA: AnyFeature[], rollB: AnyFeature[], bar?: TrackerBar): AlignmentResult;
22
- export {};
35
+ export declare function alignFeatures(rollA: readonly AnyFeature[], rollB: readonly AnyFeature[], bar?: TrackerBar): AlignmentResult | undefined;
package/lib/alignment.js CHANGED
@@ -23,12 +23,13 @@ export const applyShift = (shift, copy) => {
23
23
  copy.ops = [...copy.ops, 'shifted'];
24
24
  copy.measurements.shift = shift;
25
25
  };
26
- export const applyStretch = (paperStretch, copy) => {
26
+ /** Scales the copy's features away from the beginning of the roll, and records the factor. */
27
+ export const applyScale = (factor, copy) => {
27
28
  if (copy.ops.includes('stretched'))
28
29
  return;
29
- copy.features.forEach(feature => stretch(feature.horizontal, paperStretch.factor));
30
+ copy.features.forEach(feature => stretch(feature.horizontal, factor));
30
31
  copy.ops = [...copy.ops, 'stretched'];
31
- copy.conditions.push(paperStretch);
32
+ copy.measurements.scale = factor;
32
33
  };
33
34
  /** Takes the shift off the copy's features again, as far as one was applied. */
34
35
  export const revertShift = (copy) => {
@@ -43,67 +44,134 @@ export const revertShift = (copy) => {
43
44
  copy.ops = copy.ops.filter(op => op !== 'shifted');
44
45
  delete copy.measurements.shift;
45
46
  };
46
- const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
47
- /** Takes the stretch off the copy's features again, as far as one was applied. */
48
- export const revertStretch = (copy) => {
49
- const applied = copy.conditions.find(isPaperStretch);
50
- if (!copy.ops.includes('stretched') || !applied)
47
+ /** Takes the scale off the copy's features again, as far as one was applied. */
48
+ export const revertScale = (copy) => {
49
+ const factor = copy.measurements.scale;
50
+ if (!copy.ops.includes('stretched') || factor === undefined)
51
51
  return;
52
- copy.features.forEach(feature => stretch(feature.horizontal, 1 / applied.factor));
52
+ copy.features.forEach(feature => stretch(feature.horizontal, 1 / factor));
53
53
  copy.ops = copy.ops.filter(op => op !== 'stretched');
54
- copy.conditions = copy.conditions.filter(condition => !isPaperStretch(condition));
54
+ delete copy.measurements.scale;
55
55
  };
56
- const isNoteOn = (bar) => (feature) => {
57
- return feature.type === 'Hole' && bar.meaningOf(feature.vertical.from)?.type === 'note';
56
+ const byPlace = (x, y) => x.at - y.at || x.pitch - y.pitch;
57
+ /** The notes the bar reads off the features, in the order they pass it. */
58
+ const noteOnsets = (features, bar) => features
59
+ .flatMap((feature) => {
60
+ if (feature.type !== 'Hole')
61
+ return [];
62
+ const meaning = bar.meaningOf(feature.vertical.from);
63
+ return meaning?.type === 'note' ? [{ pitch: meaning.pitch, at: feature.horizontal.from }] : [];
64
+ })
65
+ .sort(byPlace);
66
+ const median = (values) => {
67
+ const sorted = [...values].sort((x, y) => x - y);
68
+ return sorted[Math.floor(sorted.length / 2)] ?? 0;
58
69
  };
70
+ /** The slope of the line through two matches, or nothing where they share a place on the copy. */
71
+ const slopeBetween = (m, n) => n.a === m.a ? [] : [(n.b - m.b) / (n.a - m.a)];
72
+ /** Matches beyond this are thinned before the slopes are taken, whose number grows with the square. */
73
+ const SLOPE_SAMPLE = 1500;
59
74
  /**
60
- * Fit a line: position = alpha * index + beta via least squares.
75
+ * The line of the median slope and the median intercept, after Theil
76
+ * and Sen. A minority of matches at odds with the rest, as from a
77
+ * passage the copy retimes or from an anchor set by chance, leaves it
78
+ * unmoved, where a least-squares line would tilt towards them.
61
79
  */
62
- function fitIndexToPosition(indices, positions) {
63
- const n = indices.length;
64
- const meanIdx = indices.reduce((s, i) => s + i, 0) / n;
65
- const meanPos = positions.reduce((s, p) => s + p, 0) / n;
66
- let num = 0;
67
- let den = 0;
68
- for (let i = 0; i < n; i++) {
69
- const d = indices[i] - meanIdx;
70
- num += d * (positions[i] - meanPos);
71
- den += d * d;
80
+ const robustLine = (matches) => {
81
+ const step = Math.max(1, Math.ceil(matches.length / SLOPE_SAMPLE));
82
+ const sample = matches.filter((_, i) => i % step === 0);
83
+ const slopes = sample.flatMap((m, i) => sample.slice(i + 1).flatMap(n => slopeBetween(m, n)));
84
+ if (slopes.length === 0)
85
+ return undefined;
86
+ const slope = median(slopes);
87
+ return { slope, intercept: median(matches.map(m => m.b - slope * m.a)) };
88
+ };
89
+ const placed = (line, a) => line.slope * a + line.intercept;
90
+ const residualOf = (line, match) => match.b - placed(line, match.a);
91
+ /** Pitches in a row that pin down a place on a roll, provided the row occurs once. */
92
+ const ANCHOR_LENGTH = 6;
93
+ /** Every run of `length` pitches, keyed by the run, with the onsets it starts at. */
94
+ const runsOf = (onsets, length) => {
95
+ const runs = new Map();
96
+ onsets.slice(0, Math.max(0, onsets.length - length + 1)).forEach((start, i) => {
97
+ const key = onsets.slice(i, i + length).map(onset => onset.pitch).join(',');
98
+ runs.set(key, [...(runs.get(key) ?? []), start]);
99
+ });
100
+ return runs;
101
+ };
102
+ /** The places a run of pitches found once on each roll ties together. */
103
+ const anchors = (a, b) => {
104
+ const runsB = runsOf(b, ANCHOR_LENGTH);
105
+ return [...runsOf(a, ANCHOR_LENGTH)]
106
+ .flatMap(([key, starts]) => {
107
+ const others = runsB.get(key);
108
+ return starts.length === 1 && others?.length === 1
109
+ ? [{ a: starts[0].at, b: others[0].at }]
110
+ : [];
111
+ });
112
+ };
113
+ /** Takes the candidates nearest first, each onset going into one pair only. */
114
+ const pairedNearestFirst = (candidates) => {
115
+ const taken = new Set();
116
+ const matches = [];
117
+ for (const candidate of [...candidates].sort((x, y) => x.distance - y.distance)) {
118
+ if (taken.has(candidate.a) || taken.has(candidate.b))
119
+ continue;
120
+ taken.add(candidate.a);
121
+ taken.add(candidate.b);
122
+ matches.push({ a: candidate.a.at, b: candidate.b.at });
72
123
  }
73
- const alpha = den === 0 ? 1 : num / den;
74
- const beta = meanPos - alpha * meanIdx;
75
- return { alpha, beta };
76
- }
124
+ return matches;
125
+ };
126
+ /** Each onset of A paired with the nearest onset of B of its pitch within the window around where the line puts it. */
127
+ const nearestMatches = (a, b, line, window) => {
128
+ const bByPitch = new Map();
129
+ b.forEach(onset => bByPitch.set(onset.pitch, [...(bByPitch.get(onset.pitch) ?? []), onset]));
130
+ const candidates = a.flatMap(onset => {
131
+ const expected = placed(line, onset.at);
132
+ return (bByPitch.get(onset.pitch) ?? [])
133
+ .map(other => ({ distance: Math.abs(other.at - expected), a: onset, b: other }))
134
+ .filter(candidate => candidate.distance <= window);
135
+ });
136
+ return pairedNearestFirst(candidates);
137
+ };
77
138
  /**
78
- * Selects the first and last N elements of an array (or fewer if length is smaller).
139
+ * Windows, in the other roll's millimetres, narrowed as the line settles.
140
+ * The first is wide enough to catch what a line extrapolated from the
141
+ * anchors misses at the ends of a long roll; the last is narrow enough
142
+ * to leave out a passage the copy retimed.
79
143
  */
80
- function selectEnds(arr, count) {
81
- const n = arr.length;
82
- if (count * 2 >= n)
83
- return arr.slice();
84
- return arr.slice(0, count).concat(arr.slice(n - count, n));
85
- }
144
+ const WINDOWS = [mm(40), mm(15), mm(6)];
145
+ const settled = (a, b) => (fit, window) => {
146
+ const matches = nearestMatches(a, b, fit.line, window);
147
+ return { line: robustLine(matches) ?? fit.line, matches };
148
+ };
149
+ const resultOf = ({ line, matches }) => {
150
+ if (matches.length < 2 || line.slope <= 0)
151
+ return undefined;
152
+ const squares = matches.reduce((total, m) => total + residualOf(line, m) ** 2, 0);
153
+ return {
154
+ shift: mm(line.intercept / line.slope),
155
+ scale: line.slope,
156
+ matched: matches.length,
157
+ residual: mm(Math.sqrt(squares / matches.length))
158
+ };
159
+ };
86
160
  /**
87
- * Align two rolls by computing independent linear fits of each roll's note-onset positions
88
- * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
161
+ * Finds the shift and scale that carry the places of `rollA` onto those
162
+ * of `rollB`, reading both through the bar. Runs of pitches that occur
163
+ * once on each roll anchor a first line; then every note is paired with
164
+ * its nearest counterpart of the same pitch and the line refitted, the
165
+ * window closing each time. A copy cut for another paper speed, a scan
166
+ * with a calibration pattern, and holes the other copy lacks are all
167
+ * within reach of that. Nothing is found where the rolls share no run of
168
+ * pitches, as between two different pieces.
89
169
  */
90
170
  export function alignFeatures(rollA, rollB, bar = welteT100) {
91
- // 1. Extract note-onset positions
92
- const isNote = isNoteOn(bar);
93
- const allXA = rollA.filter(isNote).map(f => f.horizontal.from);
94
- const allXB = rollB.filter(isNote).map(f => f.horizontal.from);
95
- // 2. Determine segment size (e.g. 10% of notes, min 5)
96
- const segCount = Math.max(5, Math.floor(allXA.length * 0.1));
97
- // 3. Select only first and last segments
98
- const XA = selectEnds(allXA, segCount);
99
- const idxA = XA.map((_, i) => i);
100
- const XB = selectEnds(allXB, segCount);
101
- const idxB = XB.map((_, i) => i);
102
- // 4. Fit index->position for each roll on selected ends
103
- const { alpha: alphaA, beta: betaA } = fitIndexToPosition(idxA, XA);
104
- const { alpha: alphaB, beta: betaB } = fitIndexToPosition(idxB, XB);
105
- // 5. Derive stretch and shift such that x2 = (x1 + shift) * stretch
106
- const stretch = alphaB / alphaA;
107
- const shift = mm(betaB / stretch - betaA);
108
- return { stretch, shift };
171
+ const a = noteOnsets(rollA, bar);
172
+ const b = noteOnsets(rollB, bar);
173
+ const coarse = robustLine(anchors(a, b));
174
+ if (!coarse)
175
+ return undefined;
176
+ return resultOf(WINDOWS.reduce(settled(a, b), { line: coarse, matches: [] }));
109
177
  }
@@ -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
@@ -5,7 +5,7 @@ import { isPerforation, placementRelations } from "./Symbol";
5
5
  import { collationsOf, defaultCollationTolerance } from "./Collation";
6
6
  import { insertedBy } from "./Version";
7
7
  import { asSymbols } from "./RollCopy";
8
- import { applyShift, applyStretch, revertShift, revertStretch } from "./alignment";
8
+ import { applyShift, applyScale, revertShift, revertScale } from "./alignment";
9
9
  import { assignReference, idOf } from "./Assumption";
10
10
  import { distance, mm, subtract } from "./Quantity";
11
11
  const noChange = () => undefined;
@@ -64,15 +64,37 @@ export const createVersion = (siglum, copy) => draft => {
64
64
  motivations: []
65
65
  });
66
66
  };
67
- /** Shifts and then stretches the copy's features into line with another copy's. */
68
- 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 => {
69
84
  applyShift(shift, copy);
70
- applyStretch(stretch, copy);
85
+ applyScale(scale, copy);
86
+ if (reading)
87
+ readScale(copy, reading);
71
88
  });
72
- /** 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
+ */
73
94
  export const unalignCopy = (copyId) => onCopy(copyId, copy => {
74
- revertStretch(copy);
95
+ revertScale(copy);
75
96
  revertShift(copy);
97
+ copy.conditions = without(copy.conditions, isPaperStretch);
76
98
  });
77
99
  const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
78
100
  /**
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 { readFromSpencerBar, licenseeOnT100, SPENCER_ROWS_PER_INCH, type SpencerBarOptions } from './readers/spencerBar';
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 { readFromSpencerBar, licenseeOnT100, SPENCER_ROWS_PER_INCH } from './readers/spencerBar';
29
+ export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH } from './readers/spencerBar';
package/lib/migrate.js CHANGED
@@ -49,10 +49,26 @@ const withProductionNodes = (node) => {
49
49
  ...rest,
50
50
  ...(typeof company === 'string' ? (company && { company: named(company) }) : { company }),
51
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 })
52
54
  }
53
55
  };
54
56
  };
55
- 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]
56
72
  .reduce((result, step) => step(result), node);
57
73
  /** The items each walked, or the very same list where the walk changed none. */
58
74
  const walked = (items) => {
@@ -1,5 +1,5 @@
1
- import { RollCopy } from "../RollCopy";
2
- import { Track } from "../Quantity";
1
+ import { PaperSpeed, RollCopy } from "../RollCopy";
2
+ import { TrackerBar } from "../TrackerBar";
3
3
  /**
4
4
  * Rows of the image on an inch of paper. The player reads eight rows a
5
5
  * second per unit of roll tempo (tempo 80 comes with a sample rate of
@@ -8,11 +8,27 @@ import { Track } from "../Quantity";
8
8
  * scanner kept it is for an alignment with other copies to tell.
9
9
  */
10
10
  export declare const SPENCER_ROWS_PER_INCH = 400;
11
- export declare const licenseeOnT100: (position: number) => Track;
12
11
  export interface SpencerBarOptions {
13
12
  /** Rows of the image on an inch of paper. */
14
13
  rowsPerInch?: number;
15
- /** Puts a position the file names onto the edition's bar. */
16
- trackOf?: (position: number) => Track;
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;
17
21
  }
18
- export declare function readFromSpencerBar(buffer: ArrayBuffer, { rowsPerInch, trackOf }?: SpencerBarOptions): RollCopy;
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;
@@ -1,6 +1,8 @@
1
1
  import { v4 } from "uuid";
2
+ import { systemOf, translationBetween } from "../TrackerBar";
2
3
  import { welteT100 } from "../systems/welteT100/bar";
3
- import { inMillimeters, px, track } from "../Quantity";
4
+ import { welteLicensee } from "../systems/welteLicensee/bar";
5
+ import { feetPerMinute, inMillimeters, px, track } from "../Quantity";
4
6
  /**
5
7
  * Spencer Chase's e-roll file (`.bar`, "eRoll Tracker Bar Image") holds
6
8
  * a roll as a list of events: a distance in rows of the scanned image,
@@ -23,16 +25,6 @@ const END_OF_EVENTS = 0xFF;
23
25
  * scanner kept it is for an alignment with other copies to tell.
24
26
  */
25
27
  export const SPENCER_ROWS_PER_INCH = 400;
26
- /**
27
- * The file numbers positions as the 98-hole Welte Licensee bar does.
28
- * Its bass controls are the T-100's tracks 1 to 8, it has no motor
29
- * tracks, and its note block follows the controls at once, so from
30
- * there on every position lies two tracks lower than on the T-100 bar.
31
- * Checked valve by valve on roll 225 against the Stanford copies.
32
- */
33
- const LICENSEE_NOTES_FROM = 9;
34
- const T100_NOTES_FROM = welteT100.areas.find(area => area.role === 'note').from;
35
- export const licenseeOnT100 = (position) => track(position < LICENSEE_NOTES_FROM ? position : position + T100_NOTES_FROM - LICENSEE_NOTES_FROM);
36
28
  const byteAt = (bytes, at) => {
37
29
  const byte = bytes[at];
38
30
  if (byte === undefined)
@@ -87,26 +79,36 @@ const holesOf = (events) => {
87
79
  }
88
80
  return holes.sort((a, b) => a.from - b.from);
89
81
  };
90
- export function readFromSpencerBar(buffer, { rowsPerInch = SPENCER_ROWS_PER_INCH, trackOf = licenseeOnT100 } = {}) {
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 } = {}) {
91
87
  const bytes = new Uint8Array(buffer);
92
88
  if (byteAt(bytes, TEXT_AT) !== TEXT_TAG) {
93
89
  throw new Error('Not a Spencer .bar file: no text after the header');
94
90
  }
95
91
  const placeOf = (row) => inMillimeters(px(row), rowsPerInch);
92
+ const onBar = translationBetween(system, bar);
96
93
  const features = holesOf(eventsIn(bytes, endOfText(bytes, TEXT_AT + 1)))
97
- .map((hole) => ({
98
- type: 'Hole',
99
- id: v4(),
100
- vertical: {
101
- from: trackOf(hole.position),
102
- unit: 'track'
103
- },
104
- horizontal: {
105
- unit: 'mm',
106
- from: placeOf(hole.from),
107
- to: placeOf(hole.to)
108
- }
109
- }));
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
+ });
110
112
  return {
111
113
  type: 'RollCopy',
112
114
  id: v4(),
@@ -114,7 +116,26 @@ export function readFromSpencerBar(buffer, { rowsPerInch = SPENCER_ROWS_PER_INCH
114
116
  conditions: [],
115
117
  keeper: { name: '', sameAs: [] },
116
118
  measurements: {},
119
+ production: { system: systemOf(system) },
117
120
  modifications: [],
118
121
  features
119
122
  };
120
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;
@@ -1,5 +1,6 @@
1
1
  import { v4 } from "uuid";
2
2
  import { AtonParser } from "./AtonParser";
3
+ import { systemOf, translationBetween } from "../TrackerBar";
3
4
  import { welteT100 } from "../systems/welteT100/bar";
4
5
  import { inMillimeters, mean, mm, px, subtract, track } from "../Quantity";
5
6
  /** Values in these files carry their unit as a suffix, e.g. "37.7646px". */
@@ -82,7 +83,7 @@ const measuredByOf = (rollinfo) => {
82
83
  date
83
84
  };
84
85
  };
85
- export function readFromStanfordAton(atonString, { trackShift, bar = welteT100, scan } = {}) {
86
+ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100, system = bar, scan } = {}) {
86
87
  const parser = new AtonParser();
87
88
  const json = parser.parse(atonString);
88
89
  const holes = json.ROLLINFO.HOLES.HOLE;
@@ -93,7 +94,7 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
93
94
  const measuredBy = measuredByOf(json.ROLLINFO);
94
95
  const rewindTrack = rewindTrackIn(holes);
95
96
  const shift = trackShift
96
- ?? (rewindTrack === undefined ? track(0) : track(bar.rewindTrack - rewindTrack));
97
+ ?? (rewindTrack === undefined ? track(0) : track(system.rewindTrack - rewindTrack));
97
98
  const calibration = {
98
99
  unit: 'px',
99
100
  offset: gridOffsetOf(holes, separation, json.ROLLINFO.HOLE_OFFSET),
@@ -102,26 +103,30 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
102
103
  };
103
104
  const punchDiameter = punchDiameterOf(holes, dpi);
104
105
  const chains = chainsAmong([...holes, ...chainedBadHoles(listOf(json.ROLLINFO.BADHOLES?.HOLE), calibration)]);
106
+ const onBar = translationBetween(system, bar);
105
107
  const features = chains
106
- .map(({ hole, attack, release }) => {
108
+ .flatMap(({ hole, attack, release }) => {
109
+ const position = onBar(track(+hole.TRACKER_HOLE + shift));
110
+ if (position === undefined)
111
+ return [];
107
112
  const column = readPx(hole.ORIGIN_COL);
108
113
  const columnWidth = readPx(hole.WIDTH_COL);
109
- return {
110
- type: 'Hole',
111
- id: v4(),
112
- ...(stanford && {
113
- depiction: stanford.depictionOf(column, attack, columnWidth, subtract(release, attack))
114
- }),
115
- vertical: {
116
- from: track(+hole.TRACKER_HOLE + shift),
117
- unit: 'track'
118
- },
119
- horizontal: {
120
- unit: 'mm',
121
- from: inMillimeters(attack, dpi),
122
- to: inMillimeters(release, dpi)
123
- }
124
- };
114
+ return [{
115
+ type: 'Hole',
116
+ id: v4(),
117
+ ...(stanford && {
118
+ depiction: stanford.depictionOf(column, attack, columnWidth, subtract(release, attack))
119
+ }),
120
+ vertical: {
121
+ from: position,
122
+ unit: 'track'
123
+ },
124
+ horizontal: {
125
+ unit: 'mm',
126
+ from: inMillimeters(attack, dpi),
127
+ to: inMillimeters(release, dpi)
128
+ }
129
+ }];
125
130
  });
126
131
  return {
127
132
  type: 'RollCopy',
@@ -129,6 +134,7 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
129
134
  ops: [],
130
135
  conditions: [],
131
136
  keeper: { name: '', sameAs: [] },
137
+ production: { system: systemOf(system) },
132
138
  modifications: [],
133
139
  ...((scan ?? stanford) && { scan: scan ?? stanford?.scan }),
134
140
  measurements: {
package/lib/schema.json CHANGED
@@ -1239,6 +1239,83 @@
1239
1239
  ],
1240
1240
  "type": "object"
1241
1241
  },
1242
+ "ObjectAssumption<PaperSpeed>": {
1243
+ "anyOf": [
1244
+ {
1245
+ "properties": {
1246
+ "@annotation": {
1247
+ "description": "An optional annotation expressing a belief about this assumption. Uses the JSON-LD-star `@annotation` mechanism to attach epistemic metadata (certainty and reasons) to any triple.",
1248
+ "properties": {
1249
+ "belief": {
1250
+ "$ref": "#/definitions/Belief",
1251
+ "description": "The belief held about the annotated statement. [ontology: crminf:J4i is subject of]"
1252
+ },
1253
+ "@id": {
1254
+ "description": "A unique identifier for this object.",
1255
+ "type": "string"
1256
+ }
1257
+ },
1258
+ "required": [
1259
+ "belief",
1260
+ "@id"
1261
+ ],
1262
+ "type": "object"
1263
+ },
1264
+ "unit": {
1265
+ "const": "ft/min",
1266
+ "description": "The unit of measurement. [ontology: crm:P91 has unit]",
1267
+ "type": "string"
1268
+ },
1269
+ "value": {
1270
+ "$ref": "#/definitions/Quantity%3C%22ft%2Fmin%22%3E",
1271
+ "description": "The measured value. [ontology: crm:P90 has value]"
1272
+ }
1273
+ },
1274
+ "required": [
1275
+ "unit",
1276
+ "value"
1277
+ ],
1278
+ "type": "object"
1279
+ },
1280
+ {
1281
+ "properties": {
1282
+ "@annotation": {
1283
+ "description": "An optional annotation expressing a belief about this assumption. Uses the JSON-LD-star `@annotation` mechanism to attach epistemic metadata (certainty and reasons) to any triple.",
1284
+ "properties": {
1285
+ "belief": {
1286
+ "$ref": "#/definitions/Belief",
1287
+ "description": "The belief held about the annotated statement. [ontology: crminf:J4i is subject of]"
1288
+ },
1289
+ "@id": {
1290
+ "description": "A unique identifier for this object.",
1291
+ "type": "string"
1292
+ }
1293
+ },
1294
+ "required": [
1295
+ "belief",
1296
+ "@id"
1297
+ ],
1298
+ "type": "object"
1299
+ },
1300
+ "unit": {
1301
+ "const": "m/min",
1302
+ "description": "The unit of measurement. [ontology: crm:P91 has unit]",
1303
+ "type": "string"
1304
+ },
1305
+ "value": {
1306
+ "$ref": "#/definitions/Quantity%3C%22m%2Fmin%22%3E",
1307
+ "description": "The measured value. [ontology: crm:P90 has value]"
1308
+ }
1309
+ },
1310
+ "required": [
1311
+ "unit",
1312
+ "value"
1313
+ ],
1314
+ "type": "object"
1315
+ }
1316
+ ],
1317
+ "description": "An object assumption wraps a complex object with an optional annotation. Used for structured values (e.g. persons, conditions) whose properties may be uncertain."
1318
+ },
1242
1319
  "ObjectAssumption<Person>": {
1243
1320
  "description": "An object assumption wraps a complex object with an optional annotation. Used for structured values (e.g. persons, conditions) whose properties may be uncertain.",
1244
1321
  "properties": {
@@ -1462,7 +1539,7 @@
1462
1539
  "type": "object"
1463
1540
  },
1464
1541
  "ProductionEvent": {
1465
- "description": "Describes the production of a roll copy: the manufacturer, the paper used, and the date. [ontology: lrmoo:F32 Item Production Event]",
1542
+ "description": "Describes the production of a roll copy: the manufacturer, the paper used, the date, and the system and paper speed the copy was cut for. [ontology: lrmoo:F32 Item Production Event]",
1466
1543
  "properties": {
1467
1544
  "company": {
1468
1545
  "$ref": "#/definitions/Agent",
@@ -1475,6 +1552,14 @@
1475
1552
  "paper": {
1476
1553
  "$ref": "#/definitions/Concept",
1477
1554
  "description": "The paper the roll copy was cut on. [ontology: crm:P126 employed]"
1555
+ },
1556
+ "speed": {
1557
+ "$ref": "#/definitions/ObjectAssumption%3CPaperSpeed%3E",
1558
+ "description": "The paper speed the copy was cut for. A copy cut from the same master for another speed comes out longer or shorter than the roll it is aligned with by the ratio of the speeds, which is what the alignment then measures. [ontology: reo:paperSpeed]"
1559
+ },
1560
+ "system": {
1561
+ "$ref": "#/definitions/Concept",
1562
+ "description": "The reproducing system the copy was cut for. Left out, it is the roll's own; a Licensee re-cut of a T-100 roll names the Licensee here. A system the type vocabulary knows carries the IRI of its concept as `id`. [ontology: crm:P32 used general technique]"
1478
1563
  }
1479
1564
  },
1480
1565
  "type": "object"
@@ -1694,6 +1779,10 @@
1694
1779
  "$ref": "#/definitions/Measure%3C%22mm%22%3E",
1695
1780
  "description": "The average diameter of punched holes. [ontology: reo:punchDiameter]"
1696
1781
  },
1782
+ "scale": {
1783
+ "description": "The factor this copy's features were scaled by to align them with the others. What it is put down to is stated apart: a paper-stretch condition, or the speed the copy was cut for. Not exported to RDF.",
1784
+ "type": "number"
1785
+ },
1697
1786
  "shift": {
1698
1787
  "$ref": "#/definitions/Shift",
1699
1788
  "description": "The shift applied to align this copy with the others. Not exported to RDF."
@@ -123,6 +123,7 @@
123
123
  "@id": "crm:P39i_was_measured_by",
124
124
  "@context": {
125
125
  "shift": null,
126
+ "scale": null,
126
127
  "trackCalibration": null,
127
128
  "margins": null,
128
129
  "dimensions": "reo:dimensions",
@@ -136,7 +137,16 @@
136
137
  "version": "owl:versionInfo"
137
138
  }
138
139
  },
139
- "production": "lrmoo:R28i_was_produced_by",
140
+ "production": {
141
+ "@id": "lrmoo:R28i_was_produced_by",
142
+ "@context": {
143
+ "system": "crm:P32_used_general_technique",
144
+ "speed": {
145
+ "@id": "reo:paperSpeed",
146
+ "@context": { "value": "crm:P90_has_value" }
147
+ }
148
+ }
149
+ },
140
150
  "company": "crm:P14_carried_out_by",
141
151
  "paper": "crm:P126_employed",
142
152
  "conditions": "crm:P44_has_condition",
@@ -0,0 +1,6 @@
1
+ import { Concept } from "../Agent";
2
+ import { TrackerBar } from "../TrackerBar";
3
+ /** The tracker bars the library knows, the T-100 first as the usual one. */
4
+ export declare const trackerBars: readonly TrackerBar[];
5
+ /** The bar of a system the type vocabulary knows, from its concept. */
6
+ export declare const trackerBarOf: (system: Concept | undefined) => TrackerBar | undefined;
@@ -0,0 +1,10 @@
1
+ import { systemIdOf } from "../TrackerBar";
2
+ import { welteT100 } from "./welteT100/bar";
3
+ import { welteLicensee } from "./welteLicensee/bar";
4
+ /** The tracker bars the library knows, the T-100 first as the usual one. */
5
+ export const trackerBars = [welteT100, welteLicensee];
6
+ /** The bar of a system the type vocabulary knows, from its concept. */
7
+ export const trackerBarOf = (system) => {
8
+ const id = systemIdOf(system);
9
+ return trackerBars.find(bar => bar.id === id);
10
+ };
@@ -0,0 +1,11 @@
1
+ import { TrackerBar } from "../../TrackerBar";
2
+ /**
3
+ * Welte-Mignon (Licensee), the American re-cut of the T-100 rolls on
4
+ * a roll 11¼ inches wide with 98 positions at nine to the inch, cf.
5
+ * Hagmann, p. 40 f. and Phillips, p. 123. It reads the same commands
6
+ * as the T-100 and lays them out the same way, minus the two motor
7
+ * tracks, so the note block and the treble valves sit two positions
8
+ * lower. The layout is the one Stanford's midi2exp reads, checked
9
+ * valve by valve on roll 225 against the T-100 copies.
10
+ */
11
+ export declare const welteLicensee: TrackerBar;
@@ -0,0 +1,38 @@
1
+ import { describeTrackerBar } from "../../TrackerBar";
2
+ import { mm } from "../../Quantity";
3
+ /**
4
+ * Welte-Mignon (Licensee), the American re-cut of the T-100 rolls on
5
+ * a roll 11¼ inches wide with 98 positions at nine to the inch, cf.
6
+ * Hagmann, p. 40 f. and Phillips, p. 123. It reads the same commands
7
+ * as the T-100 and lays them out the same way, minus the two motor
8
+ * tracks, so the note block and the treble valves sit two positions
9
+ * lower. The layout is the one Stanford's midi2exp reads, checked
10
+ * valve by valve on roll 225 against the T-100 copies.
11
+ */
12
+ export const welteLicensee = describeTrackerBar({
13
+ id: 'welte-licensee',
14
+ name: 'Welte-Mignon (Licensee)',
15
+ width: mm(285.75),
16
+ trackCount: 98,
17
+ notes: { from: 9, to: 88, lowestPitch: 24 },
18
+ expressions: new Map([
19
+ [1, 'MezzoforteOff'],
20
+ [2, 'MezzoforteOn'],
21
+ [3, 'SlowCrescendoOff'],
22
+ [4, 'SlowCrescendoOn'],
23
+ [5, 'ForzandoOff'],
24
+ [6, 'ForzandoOn'],
25
+ [7, 'SoftPedalOff'],
26
+ [8, 'SoftPedalOn'],
27
+ [89, 'Rewind'],
28
+ [90, 'ElectricCutOff'],
29
+ [91, 'SustainPedalOn'],
30
+ [92, 'SustainPedalOff'],
31
+ [93, 'ForzandoOn'],
32
+ [94, 'ForzandoOff'],
33
+ [95, 'SlowCrescendoOn'],
34
+ [96, 'SlowCrescendoOff'],
35
+ [97, 'MezzoforteOn'],
36
+ [98, 'MezzoforteOff']
37
+ ])
38
+ });
@@ -9,6 +9,7 @@ export type WelteT100ExpressionType = typeof welteT100ExpressionTypes[number];
9
9
  *
10
10
  * The note block spans 80 positions from C1 to g⁴, i.e. MIDI 24 to 103.
11
11
  * The expression valves are duplicated, bass below the note block and
12
- * treble above it, in mirrored order.
12
+ * treble above it, in mirrored order. The rolls run at three metres a
13
+ * minute (Phillips 2016, p. 113; Bärtsch 2020 gives 3 or 2.9).
13
14
  */
14
15
  export declare const welteT100: TrackerBar;
@@ -1,5 +1,5 @@
1
1
  import { describeTrackerBar } from "../../TrackerBar";
2
- import { mm } from "../../Quantity";
2
+ import { metersPerMinute, mm } from "../../Quantity";
3
3
  /**
4
4
  * The commands of the Welte-Mignon T-100, as its tracker bar reads them.
5
5
  */
@@ -24,7 +24,8 @@ export const welteT100ExpressionTypes = [
24
24
  *
25
25
  * The note block spans 80 positions from C1 to g⁴, i.e. MIDI 24 to 103.
26
26
  * The expression valves are duplicated, bass below the note block and
27
- * treble above it, in mirrored order.
27
+ * treble above it, in mirrored order. The rolls run at three metres a
28
+ * minute (Phillips 2016, p. 113; Bärtsch 2020 gives 3 or 2.9).
28
29
  */
29
30
  export const welteT100 = describeTrackerBar({
30
31
  id: 'welte-t100',
@@ -32,6 +33,7 @@ export const welteT100 = describeTrackerBar({
32
33
  width: mm(328),
33
34
  trackCount: 100,
34
35
  notes: { from: 11, to: 90, lowestPitch: 24 },
36
+ paperSpeed: { value: metersPerMinute(3), unit: 'm/min' },
35
37
  expressions: new Map([
36
38
  [1, 'MezzoforteOff'],
37
39
  [2, 'MezzoforteOn'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.11.0",
3
+ "version": "0.13.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": {