linked-rolls 0.0.1 → 0.1.1
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/README.md +48 -1
- package/lib/Assumption.d.ts +124 -0
- package/lib/Assumption.js +34 -0
- package/lib/Collation.d.ts +11 -0
- package/lib/ConditionState.d.ts +8 -3
- package/lib/ConditionState.js +0 -5
- package/lib/Edit.d.ts +44 -14
- package/lib/Edit.js +2 -95
- package/lib/Edition.d.ts +144 -4
- package/lib/EditionView.d.ts +51 -0
- package/lib/EditionView.js +294 -0
- package/lib/Emulation.d.ts +21 -68
- package/lib/Emulation.js +94 -386
- package/lib/Feature.d.ts +137 -16
- package/lib/Feature.js +9 -1
- package/lib/Plan.d.ts +190 -0
- package/lib/Plan.js +555 -0
- package/lib/ReproducingSystem.d.ts +80 -0
- package/lib/RollCopy.d.ts +265 -24
- package/lib/RollCopy.js +276 -215
- package/lib/Symbol.d.ts +70 -37
- package/lib/Symbol.js +2 -27
- package/lib/TrackCalibration.d.ts +33 -0
- package/lib/TrackCalibration.js +14 -0
- package/lib/TrackerBar.d.ts +52 -5
- package/lib/TrackerBar.js +70 -49
- package/lib/Version.d.ts +32 -33
- package/lib/Version.js +3 -209
- package/lib/alignFeatures.d.ts +3 -2
- package/lib/alignFeatures.js +5 -4
- package/lib/asJsonLd.d.ts +1 -1
- package/lib/asJsonLd.js +13 -26
- package/lib/importJsonLd.d.ts +0 -1
- package/lib/importJsonLd.js +9 -72
- package/lib/index.d.ts +7 -3
- package/lib/index.js +7 -3
- package/lib/schema.json +1739 -0
- package/lib/spec/context.json +125 -144
- package/lib/systems/welteT100.d.ts +55 -0
- package/lib/systems/welteT100.js +191 -0
- package/lib/utils.d.ts +29 -0
- package/lib/validate.d.ts +3 -0
- package/lib/validate.js +10 -0
- package/package.json +41 -8
- package/lib/Condition.d.ts +0 -10
- package/lib/EditorialAssumption.d.ts +0 -67
- package/lib/EditorialAssumption.js +0 -26
- package/lib/Measurement.d.ts +0 -9
- package/lib/PlaceTimeConversion.d.ts +0 -65
- package/lib/PlaceTimeConversion.js +0 -175
- package/lib/RollEvent.d.ts +0 -76
- package/lib/RollEvent.js +0 -3
- package/lib/Stage.d.ts +0 -37
- package/lib/Stage.js +0 -165
- package/lib/Transcription.d.ts +0 -7
- package/lib/Transcription.js +0 -9
- package/lib/WithId.d.ts +0 -3
- package/lib/WithId.js +0 -1
- package/lib/alignRolls.d.ts +0 -7
- package/lib/alignRolls.js +0 -49
- package/lib/alignSymbols.d.ts +0 -7
- package/lib/alignSymbols.js +0 -49
- package/lib/aton/AtonParser.test.d.ts +0 -1
- package/lib/aton/AtonParser.test.js +0 -16
- package/lib/build-schema.cjs +0 -113
- /package/lib/{Condition.js → ReproducingSystem.js} +0 -0
- /package/lib/{Measurement.js → utils.js} +0 -0
package/lib/RollCopy.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { AtonParser } from "./aton/AtonParser";
|
|
2
2
|
import { v4 } from "uuid";
|
|
3
|
-
import { assign, flat } from "./EditorialAssumption";
|
|
4
3
|
import { read } from "midifile-ts";
|
|
5
4
|
import { asSpans } from "./asMIDISpans";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
const
|
|
5
|
+
import { welteT100 } from "./TrackerBar";
|
|
6
|
+
import { assignReference, idsOf } from "./Assumption";
|
|
7
|
+
export const rollConditions = [
|
|
8
|
+
'general',
|
|
9
|
+
'paper-stretch'
|
|
10
|
+
];
|
|
11
|
+
export const applyShift = (shift, copy) => {
|
|
12
|
+
if (copy.ops.includes('shifted'))
|
|
13
|
+
return;
|
|
14
|
+
const to = copy.features;
|
|
9
15
|
for (const event of to) {
|
|
10
16
|
event.horizontal.from += shift.horizontal;
|
|
11
17
|
if (event.horizontal.to) {
|
|
@@ -16,255 +22,310 @@ const applyShift = (shift, to) => {
|
|
|
16
22
|
event.vertical.to += shift.vertical;
|
|
17
23
|
}
|
|
18
24
|
}
|
|
25
|
+
copy.ops = [...copy.ops, 'shifted'];
|
|
26
|
+
copy.measurements.shift = shift;
|
|
19
27
|
};
|
|
20
|
-
const applyStretch = (
|
|
28
|
+
export const applyStretch = (paperStretch, copy) => {
|
|
29
|
+
if (copy.ops.includes('stretched'))
|
|
30
|
+
return;
|
|
31
|
+
const stretch = paperStretch.factor;
|
|
32
|
+
const to = copy.features;
|
|
21
33
|
for (const event of to) {
|
|
22
34
|
event.horizontal.from *= stretch;
|
|
23
35
|
if (event.horizontal.to) {
|
|
24
36
|
event.horizontal.to *= stretch;
|
|
25
37
|
}
|
|
26
38
|
}
|
|
39
|
+
copy.ops = [...copy.ops, 'stretched'];
|
|
40
|
+
copy.conditions.push(paperStretch);
|
|
27
41
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
value: {}
|
|
47
|
-
});
|
|
48
|
-
Object.defineProperty(this, "productionEvent", {
|
|
49
|
-
enumerable: true,
|
|
50
|
-
configurable: true,
|
|
51
|
-
writable: true,
|
|
52
|
-
value: void 0
|
|
53
|
-
});
|
|
54
|
-
Object.defineProperty(this, "conditions", {
|
|
55
|
-
enumerable: true,
|
|
56
|
-
configurable: true,
|
|
57
|
-
writable: true,
|
|
58
|
-
value: []
|
|
59
|
-
});
|
|
60
|
-
Object.defineProperty(this, "location", {
|
|
61
|
-
enumerable: true,
|
|
62
|
-
configurable: true,
|
|
63
|
-
writable: true,
|
|
64
|
-
value: ''
|
|
65
|
-
});
|
|
66
|
-
/**
|
|
67
|
-
* Provides a reconstructed version of the roll,
|
|
68
|
-
* with shift, stretch and emendations already
|
|
69
|
-
* taken into account. This property will not be
|
|
70
|
-
* exported in the final JSON.
|
|
71
|
-
*/
|
|
72
|
-
Object.defineProperty(this, "features", {
|
|
73
|
-
enumerable: true,
|
|
74
|
-
configurable: true,
|
|
75
|
-
writable: true,
|
|
76
|
-
value: []
|
|
77
|
-
});
|
|
78
|
-
Object.defineProperty(this, "scan", {
|
|
79
|
-
enumerable: true,
|
|
80
|
-
configurable: true,
|
|
81
|
-
writable: true,
|
|
82
|
-
value: void 0
|
|
83
|
-
}); // P138i has representation => IIIF Image Link
|
|
84
|
-
}
|
|
85
|
-
insertFeature(feature) {
|
|
86
|
-
this.measurements.shift && applyShift(this.measurements.shift, [feature]);
|
|
87
|
-
const stretch = flat(this.conditions)
|
|
88
|
-
.find(state => state.type === 'paper-stretch');
|
|
89
|
-
if (stretch) {
|
|
90
|
-
applyStretch(stretch.factor, [feature]);
|
|
91
|
-
}
|
|
92
|
-
this.features.push(feature);
|
|
93
|
-
}
|
|
94
|
-
setShift(shift) {
|
|
95
|
-
this.measurements.shift = shift;
|
|
96
|
-
applyShift(shift, this.features);
|
|
97
|
-
}
|
|
98
|
-
setStretch(stretch) {
|
|
99
|
-
this.conditions.push(stretch);
|
|
100
|
-
applyStretch(flat(stretch).factor, this.features);
|
|
101
|
-
}
|
|
102
|
-
shallowClone() {
|
|
103
|
-
const copy = new RollCopy();
|
|
104
|
-
copy.id = this.id;
|
|
105
|
-
copy.measurements = { ...this.measurements };
|
|
106
|
-
copy.productionEvent = this.productionEvent;
|
|
107
|
-
copy.location = this.location;
|
|
108
|
-
copy.conditions = [...this.conditions];
|
|
109
|
-
copy.scan = this.scan;
|
|
110
|
-
copy.features = [...this.features];
|
|
111
|
-
return copy;
|
|
112
|
-
}
|
|
42
|
+
/**
|
|
43
|
+
* Reads the features of a copy as the tracker bar would read them.
|
|
44
|
+
* Holes on a position the bar does not read carry no symbol and are
|
|
45
|
+
* dropped, which is what happens physically as well.
|
|
46
|
+
*/
|
|
47
|
+
export function asSymbols(features, bar = welteT100) {
|
|
48
|
+
return features
|
|
49
|
+
.filter(feature => feature.type === 'Hole')
|
|
50
|
+
.flatMap((feature) => {
|
|
51
|
+
const meaning = bar.meaningOf(feature.vertical.from);
|
|
52
|
+
if (!meaning)
|
|
53
|
+
return [];
|
|
54
|
+
return [{
|
|
55
|
+
id: `symbol_${v4()}`,
|
|
56
|
+
...meaning,
|
|
57
|
+
carriers: [assignReference(feature.id)]
|
|
58
|
+
}];
|
|
59
|
+
});
|
|
113
60
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The tracks a copy carries holes on that the tracker bar does not read.
|
|
63
|
+
* A non-empty result usually means the scan is calibrated wrongly.
|
|
64
|
+
*/
|
|
65
|
+
export function unreadTracks(features, bar = welteT100) {
|
|
66
|
+
const counts = new Map();
|
|
67
|
+
features
|
|
68
|
+
.filter(feature => feature.type === 'Hole')
|
|
69
|
+
.filter(feature => !bar.meaningOf(feature.vertical.from))
|
|
70
|
+
.forEach(feature => {
|
|
71
|
+
const track = feature.vertical.from;
|
|
72
|
+
counts.set(track, (counts.get(track) || 0) + 1);
|
|
121
73
|
});
|
|
74
|
+
return counts;
|
|
122
75
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Falls back to the way the scan geometry was reconstructed before the
|
|
78
|
+
* calibration was recorded: the bass hard margin stood in for the phase
|
|
79
|
+
* of the tracker grid, with a constant making up most of the difference.
|
|
80
|
+
*
|
|
81
|
+
* The constant of one and a half tracks is the one the facsimile tiles
|
|
82
|
+
* were laid out with, and on the rolls measured so far it comes within
|
|
83
|
+
* about a quarter of a track. It is kept for copies imported before the
|
|
84
|
+
* calibration was written down; anything read since carries its own.
|
|
85
|
+
*/
|
|
86
|
+
export const calibrationOf = (copy) => {
|
|
87
|
+
if (copy.measurements.trackCalibration) {
|
|
88
|
+
return copy.measurements.trackCalibration;
|
|
126
89
|
}
|
|
90
|
+
const separation = copy.measurements.holeSeparation?.value;
|
|
91
|
+
const bassMargin = copy.measurements.margins?.bass;
|
|
92
|
+
if (separation === undefined || bassMargin === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
return {
|
|
95
|
+
unit: 'px',
|
|
96
|
+
offset: bassMargin + 1.5 * separation,
|
|
97
|
+
separation,
|
|
98
|
+
shift: 0
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
/** Values in these files carry their unit as a suffix, e.g. "37.7646px". */
|
|
102
|
+
const px = (value) => parseFloat(value);
|
|
103
|
+
/** The parser writes one record as an object and several as an array. */
|
|
104
|
+
const listOf = (value) => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
105
|
+
/**
|
|
106
|
+
* Holes the parser set aside as suspicious after it had already chained
|
|
107
|
+
* them into a note: the head of such a chain is usually a punch that
|
|
108
|
+
* came out a little short, and leaving it out would lose the whole
|
|
109
|
+
* chain. They carry no track number of their own, so the column says
|
|
110
|
+
* which track they sit on.
|
|
111
|
+
*/
|
|
112
|
+
const chainedBadHoles = (holes, calibration) => holes
|
|
113
|
+
.filter(hole => hole.NOTE_ATTACK && hole.OFF_TIME)
|
|
114
|
+
.map(hole => ({
|
|
115
|
+
...hole,
|
|
116
|
+
TRACKER_HOLE: `${Math.round((px(hole.CENTROID_COL) - calibration.offset) / calibration.separation)}`
|
|
117
|
+
}));
|
|
118
|
+
const millimeters = (pixels, dpi) => pixels / dpi * 25.4;
|
|
119
|
+
const median = (values) => {
|
|
120
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
121
|
+
const middle = Math.floor(sorted.length / 2);
|
|
122
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
123
|
+
};
|
|
124
|
+
const mostFrequent = (values) => {
|
|
125
|
+
const counts = values.reduce((acc, value) => acc.set(value, (acc.get(value) || 0) + 1), new Map());
|
|
126
|
+
return [...counts].sort(([, a], [, b]) => b - a)[0]?.[0];
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Only the first hole of a chain carries an attack and an off time;
|
|
130
|
+
* the rest continue it. The rewind perforation carries neither, and it
|
|
131
|
+
* is the last thing punched on the roll, so whatever the holes past the
|
|
132
|
+
* final musical attack sit on is the rewind track.
|
|
133
|
+
*/
|
|
134
|
+
const rewindTrackIn = (holes) => {
|
|
135
|
+
const lastMusical = holes.findLastIndex(hole => hole.NOTE_ATTACK);
|
|
136
|
+
const trailing = holes.slice(lastMusical + 1).map(hole => +hole.TRACKER_HOLE);
|
|
137
|
+
return mostFrequent(trailing);
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The phase of the tracker grid within the image. The analysis file
|
|
141
|
+
* usually states it; where it does not, the holes themselves give it away,
|
|
142
|
+
* since each sits close to the centre of its column.
|
|
143
|
+
*/
|
|
144
|
+
const gridOffsetOf = (holes, separation, stated) => {
|
|
145
|
+
if (stated !== undefined)
|
|
146
|
+
return px(stated);
|
|
147
|
+
return median(holes.map(hole => px(hole.CENTROID_COL) - +hole.TRACKER_HOLE * separation));
|
|
148
|
+
};
|
|
149
|
+
const punchDiameterOf = (holes, dpi) => {
|
|
150
|
+
const circular = holes
|
|
151
|
+
.filter(hole => px(hole.CIRCULARITY) > 0.95)
|
|
152
|
+
.map(hole => millimeters(px(hole.PERIMETER), dpi) / Math.PI);
|
|
153
|
+
if (!circular.length)
|
|
154
|
+
return undefined;
|
|
155
|
+
return circular.reduce((sum, diameter) => sum + diameter, 0) / circular.length;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* The image service Stanford keeps for a scan, and a crop of a
|
|
159
|
+
* feature from it.
|
|
160
|
+
*/
|
|
161
|
+
const stanfordScan = (druid) => ({
|
|
162
|
+
scan: `https://stacks.stanford.edu/image/iiif/${druid}%2F${druid}_0001/`,
|
|
163
|
+
depictionOf: (column, row, width, height) => `https://stacks.stanford.edu/image/iiif/${druid}/${druid}_0001/${column},${row},${width},${height}/128,/270/default.jpg`
|
|
164
|
+
});
|
|
165
|
+
/**
|
|
166
|
+
* The software behind an analysis and when it was run, as the
|
|
167
|
+
* analysis file states them.
|
|
168
|
+
*/
|
|
169
|
+
const measuredByOf = (rollinfo) => {
|
|
170
|
+
const date = new Date(rollinfo.ANALYSIS_DATE);
|
|
171
|
+
if (!rollinfo.HOLE_SOFTWARE || isNaN(date.getTime()))
|
|
172
|
+
return undefined;
|
|
173
|
+
return {
|
|
174
|
+
software: rollinfo.HOLE_SOFTWARE,
|
|
175
|
+
version: rollinfo.SOFTWARE_DATE ?? '',
|
|
176
|
+
date
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
export function readFromStanfordAton(atonString, { trackShift, bar = welteT100, scan } = {}) {
|
|
127
180
|
const parser = new AtonParser();
|
|
128
181
|
const json = parser.parse(atonString);
|
|
129
182
|
const holes = json.ROLLINFO.HOLES.HOLE;
|
|
130
183
|
const druid = json.ROLLINFO.DRUID;
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
width: rollWidth,
|
|
144
|
-
height: rollHeight,
|
|
145
|
-
unit: 'mm'
|
|
146
|
-
},
|
|
147
|
-
punchDiameter: {
|
|
148
|
-
value: averagePunchDiameter,
|
|
149
|
-
unit: 'mm'
|
|
150
|
-
},
|
|
151
|
-
holeSeparation: {
|
|
152
|
-
value: holeSeparation,
|
|
153
|
-
unit: 'px'
|
|
154
|
-
},
|
|
155
|
-
margins: {
|
|
156
|
-
treble: hardMarginTreble,
|
|
157
|
-
bass: hardMarginBass,
|
|
158
|
-
unit: 'px'
|
|
159
|
-
},
|
|
160
|
-
// todo
|
|
161
|
-
shift: {
|
|
162
|
-
horizontal: 0,
|
|
163
|
-
vertical: 0
|
|
164
|
-
}
|
|
184
|
+
const stanford = druid ? stanfordScan(druid) : undefined;
|
|
185
|
+
const separation = px(json.ROLLINFO.HOLE_SEPARATION);
|
|
186
|
+
const dpi = parseFloat(json.ROLLINFO.LENGTH_DPI);
|
|
187
|
+
const measuredBy = measuredByOf(json.ROLLINFO);
|
|
188
|
+
const rewindTrack = rewindTrackIn(holes);
|
|
189
|
+
const shift = trackShift
|
|
190
|
+
?? (rewindTrack === undefined ? 0 : bar.rewindTrack - rewindTrack);
|
|
191
|
+
const calibration = {
|
|
192
|
+
unit: 'px',
|
|
193
|
+
offset: gridOffsetOf(holes, separation, json.ROLLINFO.HOLE_OFFSET),
|
|
194
|
+
separation,
|
|
195
|
+
shift
|
|
165
196
|
};
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const trackerHole = +hole.TRACKER_HOLE + rewindShift;
|
|
179
|
-
const noteAttack = +hole.NOTE_ATTACK.replace('px', '');
|
|
180
|
-
const offset = +hole.OFF_TIME.replace('px', '');
|
|
181
|
-
const height = offset - noteAttack;
|
|
182
|
-
const column = +hole.ORIGIN_COL.replace('px', '');
|
|
183
|
-
const columnWidth = +hole.WIDTH_COL.replace('px', '');
|
|
184
|
-
const annotates = `https://stacks.stanford.edu/image/iiif/${druid}/${druid}_0001/${column},${noteAttack},${columnWidth},${height}/128,/270/default.jpg`;
|
|
185
|
-
const feature = {
|
|
197
|
+
const punchDiameter = punchDiameterOf(holes, dpi);
|
|
198
|
+
const chains = [...holes, ...chainedBadHoles(listOf(json.ROLLINFO.BADHOLES?.HOLE), calibration)]
|
|
199
|
+
.filter(hole => hole.NOTE_ATTACK && hole.OFF_TIME)
|
|
200
|
+
.sort((a, b) => px(a.NOTE_ATTACK) - px(b.NOTE_ATTACK));
|
|
201
|
+
const features = chains
|
|
202
|
+
.map((hole) => {
|
|
203
|
+
const attack = px(hole.NOTE_ATTACK);
|
|
204
|
+
const release = px(hole.OFF_TIME);
|
|
205
|
+
const column = px(hole.ORIGIN_COL);
|
|
206
|
+
const columnWidth = px(hole.WIDTH_COL);
|
|
207
|
+
return {
|
|
208
|
+
type: 'Hole',
|
|
186
209
|
id: v4(),
|
|
187
|
-
|
|
210
|
+
...(stanford && {
|
|
211
|
+
depiction: stanford.depictionOf(column, attack, columnWidth, release - attack)
|
|
212
|
+
}),
|
|
188
213
|
vertical: {
|
|
189
|
-
from:
|
|
214
|
+
from: +hole.TRACKER_HOLE + shift,
|
|
190
215
|
unit: 'track'
|
|
191
216
|
},
|
|
192
217
|
horizontal: {
|
|
193
218
|
unit: 'mm',
|
|
194
|
-
from:
|
|
195
|
-
to:
|
|
219
|
+
from: millimeters(attack, dpi),
|
|
220
|
+
to: millimeters(release, dpi)
|
|
196
221
|
}
|
|
197
222
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
type: 'RollCopy',
|
|
226
|
+
id: v4(),
|
|
227
|
+
ops: [],
|
|
228
|
+
conditions: [],
|
|
229
|
+
location: '',
|
|
230
|
+
modifications: [],
|
|
231
|
+
...((scan ?? stanford) && { scan: scan ?? stanford?.scan }),
|
|
232
|
+
measurements: {
|
|
233
|
+
dimensions: {
|
|
234
|
+
width: millimeters(px(json.ROLLINFO.ROLL_WIDTH), dpi),
|
|
235
|
+
height: millimeters(px(json.ROLLINFO.IMAGE_LENGTH), dpi),
|
|
236
|
+
unit: 'mm'
|
|
237
|
+
},
|
|
238
|
+
...(punchDiameter !== undefined && {
|
|
239
|
+
punchDiameter: { value: punchDiameter, unit: 'mm' }
|
|
240
|
+
}),
|
|
241
|
+
holeSeparation: {
|
|
242
|
+
value: separation,
|
|
243
|
+
unit: 'px'
|
|
244
|
+
},
|
|
245
|
+
margins: {
|
|
246
|
+
treble: px(json.ROLLINFO.HARD_MARGIN_TREBLE),
|
|
247
|
+
bass: px(json.ROLLINFO.HARD_MARGIN_BASS),
|
|
248
|
+
unit: 'px'
|
|
249
|
+
},
|
|
250
|
+
trackCalibration: calibration,
|
|
251
|
+
...(measuredBy && { measuredBy })
|
|
252
|
+
},
|
|
253
|
+
features
|
|
254
|
+
};
|
|
205
255
|
}
|
|
206
256
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
257
|
+
* How a MIDI key number in one of Spencer Chase's roll files names a
|
|
258
|
+
* tracker bar track.
|
|
209
259
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
260
|
+
* The note block follows the obvious rule, `pitch - 13`, which puts
|
|
261
|
+
* track 11 on MIDI 24 as the T100 compass requires. The bass expression
|
|
262
|
+
* block does not: it reads two tracks high, and subtracting two is what
|
|
263
|
+
* has made these files come out right so far.
|
|
264
|
+
*
|
|
265
|
+
* The boundary between the two rules is unresolved. Taken literally the
|
|
266
|
+
* rules leave tracks 8 and 9 unreachable and jump from track 7 to track 10,
|
|
267
|
+
* which no lateral offset can produce, so at least one of them is
|
|
268
|
+
* approximate. Settling it needs a Spencer file whose expression holes
|
|
269
|
+
* can be checked against the roll, hence the option to override.
|
|
212
270
|
*/
|
|
213
|
-
export
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
271
|
+
export const spencerTrackOf = (pitch) => {
|
|
272
|
+
const track = pitch - 13;
|
|
273
|
+
return track < 10 ? track - 2 : track;
|
|
274
|
+
};
|
|
275
|
+
const MM_PER_FOOT = 304.8;
|
|
276
|
+
/**
|
|
277
|
+
* Spencer Chase's rolls seem to be scanned at a roll speed of
|
|
278
|
+
* 83 (=8.3 feet per minute). A scanner feeds the paper at one
|
|
279
|
+
* speed, so time in his files is proportional to place.
|
|
280
|
+
*/
|
|
281
|
+
export const SPENCER_FEET_PER_MINUTE = 8.3;
|
|
282
|
+
/** Place on the roll in mm after `seconds` at a constant `feetPerMinute`. */
|
|
283
|
+
export const atConstantSpeed = (feetPerMinute) => (seconds) => feetPerMinute * MM_PER_FOOT / 60 * seconds;
|
|
284
|
+
export function readFromSpencerMIDI(midiBuffer, placeAt = atConstantSpeed(SPENCER_FEET_PER_MINUTE), trackOf = spencerTrackOf) {
|
|
285
|
+
const features = asSpans(read(midiBuffer))
|
|
286
|
+
.filter(span => span.type === 'note')
|
|
287
|
+
.map((span) => ({
|
|
288
|
+
type: 'Hole',
|
|
289
|
+
id: v4(),
|
|
290
|
+
vertical: {
|
|
291
|
+
from: trackOf(span.pitch),
|
|
292
|
+
unit: 'track'
|
|
293
|
+
},
|
|
294
|
+
horizontal: {
|
|
295
|
+
from: placeAt(span.onsetMs / 1000),
|
|
296
|
+
to: placeAt(span.offsetMs / 1000),
|
|
224
297
|
unit: 'mm'
|
|
225
|
-
};
|
|
226
|
-
if (span.type !== 'note')
|
|
227
|
-
continue;
|
|
228
|
-
const midiShift = 13;
|
|
229
|
-
let trackerHole = span.pitch - midiShift;
|
|
230
|
-
// for whatever reasons, the expression tracks
|
|
231
|
-
// of the Spencer Chase's MIDI rolls are off
|
|
232
|
-
// by two on the bass-side.
|
|
233
|
-
if (trackerHole < 10) {
|
|
234
|
-
trackerHole -= 2;
|
|
235
298
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return copy;
|
|
299
|
+
}));
|
|
300
|
+
return {
|
|
301
|
+
type: 'RollCopy',
|
|
302
|
+
id: v4(),
|
|
303
|
+
ops: [],
|
|
304
|
+
conditions: [],
|
|
305
|
+
location: '',
|
|
306
|
+
measurements: {},
|
|
307
|
+
modifications: [],
|
|
308
|
+
features
|
|
309
|
+
};
|
|
248
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Moves features across the tracker bar. What the new position means is
|
|
313
|
+
* left to the tracker bar to say, since meaning belongs to the symbols
|
|
314
|
+
* read off a copy rather than to the holes themselves.
|
|
315
|
+
*/
|
|
249
316
|
export function shiftVertically(features, amount) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
for (const key in newEvent) {
|
|
255
|
-
event[key] = newEvent[key];
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch (e) {
|
|
259
|
-
console.error(e);
|
|
317
|
+
features.forEach(feature => {
|
|
318
|
+
feature.vertical.from += amount;
|
|
319
|
+
if (feature.vertical.to !== undefined) {
|
|
320
|
+
feature.vertical.to += amount;
|
|
260
321
|
}
|
|
261
|
-
}
|
|
322
|
+
});
|
|
262
323
|
}
|
|
263
324
|
export const findCopiesCarrying = (sources, symbol) => {
|
|
264
325
|
const result = new Set();
|
|
265
|
-
for (const feature of
|
|
326
|
+
for (const feature of idsOf(symbol.carriers)) {
|
|
266
327
|
for (const copy of sources) {
|
|
267
|
-
if (copy.features.
|
|
328
|
+
if (copy.features.findIndex(f => f.id === feature)) {
|
|
268
329
|
result.add(copy.id);
|
|
269
330
|
}
|
|
270
331
|
}
|
package/lib/Symbol.d.ts
CHANGED
|
@@ -1,62 +1,95 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { WithId } from "./WithId";
|
|
1
|
+
import { ReferenceAssumption, ValueAssumption } from "./Assumption";
|
|
2
|
+
import { WithId } from "./utils";
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
4
|
+
* A symbol is an abstract musical or textual entity carried by one or more
|
|
5
|
+
* physical features on the roll. Symbols are the result of interpreting
|
|
6
|
+
* the physical features (holes, writings, etc.) on the roll copies.
|
|
6
7
|
*/
|
|
7
|
-
export type CarrierAssignment = EditorialAssumption<'carrierAssignment', RollFeature>;
|
|
8
8
|
export interface Symbol<T extends string> extends WithId {
|
|
9
9
|
type: T;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* References to the physical features (e.g. holes) on the roll copies
|
|
12
|
+
* that carry this symbol. Since the association might be debatable,
|
|
13
|
+
* it can be annotated. There might be e.g. doubts about the meaning
|
|
14
|
+
* and correct transcription of a feature on a physical roll.
|
|
15
|
+
* This points to a feature by its `@id`.
|
|
16
|
+
* @see crm:P128i is carried by
|
|
17
|
+
*/
|
|
18
|
+
carriers: ReferenceAssumption[];
|
|
11
19
|
}
|
|
12
20
|
export declare const isSymbol: (object: any) => object is AnySymbol;
|
|
21
|
+
/**
|
|
22
|
+
* A perforation is a symbol that is typically encoded as a single punched
|
|
23
|
+
* hole or a group of punched holes in the physical carrier.
|
|
24
|
+
* However, it might also have different physical appearences.
|
|
25
|
+
* @see reo:C5 Perforation
|
|
26
|
+
*/
|
|
13
27
|
export interface Perforation<T extends string> extends Symbol<T> {
|
|
14
|
-
|
|
28
|
+
/**
|
|
29
|
+
* In piano rolls, perforations are often aligned with other
|
|
30
|
+
* perforations, e.g. a "crescendo off" might be logically
|
|
31
|
+
* aligned to the start of a note perforation.
|
|
32
|
+
* @see reo:P3 aligned with
|
|
33
|
+
*/
|
|
34
|
+
alignedWith?: ValueAssumption<string>;
|
|
15
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A note symbol, representing a single pitched musical event on the roll.
|
|
38
|
+
* The pitch is encoded via the tracker bar position (track number).
|
|
39
|
+
* @see reo:P6 Note
|
|
40
|
+
*/
|
|
16
41
|
export interface Note extends Perforation<'note'> {
|
|
42
|
+
/**
|
|
43
|
+
* The MIDI pitch number of the note (e.g. 60 for middle C).
|
|
44
|
+
* @see reo:P5 has pitch
|
|
45
|
+
*/
|
|
17
46
|
pitch: number;
|
|
18
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The scope of an expression perforation, indicating whether it
|
|
50
|
+
* applies to the bass or treble register of the piano.
|
|
51
|
+
* @see reo:P6 has scope
|
|
52
|
+
*/
|
|
19
53
|
export type ExpressionScope = 'bass' | 'treble';
|
|
54
|
+
/**
|
|
55
|
+
* The type of expression encoded by a perforation on the roll,
|
|
56
|
+
* e.g. "crescendo on", "crescendo off", etc. on Welte T-100
|
|
57
|
+
* rolls.
|
|
58
|
+
* @see crm:P2 has type
|
|
59
|
+
*/
|
|
20
60
|
export type ExpressionType = 'SustainPedalOn' | 'SustainPedalOff' | 'SoftPedalOn' | 'SoftPedalOff' | 'MezzoforteOff' | 'MezzoforteOn' | 'SlowCrescendoOn' | 'SlowCrescendoOff' | 'ForzandoOn' | 'ForzandoOff' | 'MotorOff' | 'MotorOn' | 'Rewind' | 'ElectricCutOff';
|
|
61
|
+
/**
|
|
62
|
+
* An expression symbol, representing a perforation on the roll
|
|
63
|
+
* that governs dynamics, pedaling, or mechanical functions of the
|
|
64
|
+
* reproducing piano. Each expression has a scope (bass or treble)
|
|
65
|
+
* and a specific expression type.
|
|
66
|
+
* @see reo:C7 Expression
|
|
67
|
+
*/
|
|
21
68
|
export interface Expression extends Perforation<'expression'> {
|
|
69
|
+
/**
|
|
70
|
+
* Whether this expression applies to the bass or treble register.
|
|
71
|
+
* @see reo:P6 has scope
|
|
72
|
+
*/
|
|
22
73
|
scope: ExpressionScope;
|
|
74
|
+
/**
|
|
75
|
+
* The specific type of expression (e.g. sustain pedal, forzando, etc.).
|
|
76
|
+
* @see crm:P2 has type
|
|
77
|
+
*/
|
|
23
78
|
expressionType: ExpressionType;
|
|
24
79
|
}
|
|
25
80
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* of the original note or expression hole anymore.
|
|
81
|
+
* A textual symbol, e.g. a label or annotation found on the roll.
|
|
82
|
+
* @see crm:E33 Linguistic Object
|
|
29
83
|
*/
|
|
30
|
-
export interface
|
|
84
|
+
export interface Text extends Symbol<'text'> {
|
|
31
85
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
86
|
+
* The text content of the symbol.
|
|
87
|
+
* @see crm:P3 has note
|
|
34
88
|
*/
|
|
35
|
-
note?: string;
|
|
36
|
-
}
|
|
37
|
-
export interface Text<T extends string> extends Symbol<T> {
|
|
38
89
|
text: string;
|
|
39
|
-
rotation?: number;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* For handwritten insertions like e. g. the
|
|
43
|
-
* perforation date in the end of a roll.
|
|
44
|
-
*/
|
|
45
|
-
export interface HandwrittenText extends Text<'handwrittenText'> {
|
|
46
|
-
pen?: 'ink' | 'pencil' | 'crayon';
|
|
47
90
|
}
|
|
48
91
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* date at the end of (later) Welte rolls.
|
|
92
|
+
* A symbol can be either a note, an expression, or a text.
|
|
93
|
+
* Notes and expressions are perforations; texts are carried by writings.
|
|
52
94
|
*/
|
|
53
|
-
export
|
|
54
|
-
}
|
|
55
|
-
export interface RollLabel extends Text<'rollLabel'> {
|
|
56
|
-
signed: boolean;
|
|
57
|
-
}
|
|
58
|
-
export type AnySymbol = Note | Expression | HandwrittenText | Stamp | Cover | RollLabel;
|
|
59
|
-
export declare const dimensionOf: (symbol: AnySymbol) => {
|
|
60
|
-
horizontal: HorizontalSpan;
|
|
61
|
-
vertical: VerticalSpan;
|
|
62
|
-
};
|
|
95
|
+
export type AnySymbol = Note | Expression | Text;
|