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 +4 -0
- package/lib/Quantity.js +3 -0
- package/lib/RollCopy.d.ts +43 -1
- package/lib/TrackerBar.d.ts +16 -1
- package/lib/TrackerBar.js +21 -0
- package/lib/alignment.d.ts +26 -13
- package/lib/alignment.js +122 -54
- package/lib/editionOps.d.ts +13 -5
- package/lib/editionOps.js +28 -6
- package/lib/index.d.ts +3 -1
- package/lib/index.js +3 -1
- package/lib/migrate.js +17 -1
- package/lib/readers/spencerBar.d.ts +22 -6
- package/lib/readers/spencerBar.js +46 -25
- package/lib/readers/stanfordAton.d.ts +9 -1
- package/lib/readers/stanfordAton.js +25 -19
- package/lib/schema.json +90 -1
- package/lib/spec/context.json +11 -1
- package/lib/systems/index.d.ts +6 -0
- package/lib/systems/index.js +10 -0
- package/lib/systems/welteLicensee/bar.d.ts +11 -0
- package/lib/systems/welteLicensee/bar.js +38 -0
- package/lib/systems/welteT100/bar.d.ts +2 -1
- package/lib/systems/welteT100/bar.js +4 -2
- package/package.json +1 -1
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
|
|
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
|
package/lib/TrackerBar.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/lib/alignment.d.ts
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
|
-
import { ObjectAssumption } from "./Assumption";
|
|
2
1
|
import { AnyFeature } from "./Feature";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
11
|
-
export declare const
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
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,
|
|
30
|
+
copy.features.forEach(feature => stretch(feature.horizontal, factor));
|
|
30
31
|
copy.ops = [...copy.ops, 'stretched'];
|
|
31
|
-
copy.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 /
|
|
52
|
+
copy.features.forEach(feature => stretch(feature.horizontal, 1 / factor));
|
|
53
53
|
copy.ops = copy.ops.filter(op => op !== 'stretched');
|
|
54
|
-
|
|
54
|
+
delete copy.measurements.scale;
|
|
55
55
|
};
|
|
56
|
-
const
|
|
57
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
*
|
|
88
|
-
*
|
|
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
|
-
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
}
|
package/lib/editionOps.d.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
import { AnyArgumentation, Certainty
|
|
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
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
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,
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
85
|
+
applyScale(scale, copy);
|
|
86
|
+
if (reading)
|
|
87
|
+
readScale(copy, reading);
|
|
71
88
|
});
|
|
72
|
-
/**
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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 {
|
|
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
|
-
/**
|
|
16
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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(
|
|
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
|
-
.
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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."
|
package/lib/spec/context.json
CHANGED
|
@@ -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":
|
|
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