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.
Files changed (67) hide show
  1. package/README.md +48 -1
  2. package/lib/Assumption.d.ts +124 -0
  3. package/lib/Assumption.js +34 -0
  4. package/lib/Collation.d.ts +11 -0
  5. package/lib/ConditionState.d.ts +8 -3
  6. package/lib/ConditionState.js +0 -5
  7. package/lib/Edit.d.ts +44 -14
  8. package/lib/Edit.js +2 -95
  9. package/lib/Edition.d.ts +144 -4
  10. package/lib/EditionView.d.ts +51 -0
  11. package/lib/EditionView.js +294 -0
  12. package/lib/Emulation.d.ts +21 -68
  13. package/lib/Emulation.js +94 -386
  14. package/lib/Feature.d.ts +137 -16
  15. package/lib/Feature.js +9 -1
  16. package/lib/Plan.d.ts +190 -0
  17. package/lib/Plan.js +555 -0
  18. package/lib/ReproducingSystem.d.ts +80 -0
  19. package/lib/RollCopy.d.ts +265 -24
  20. package/lib/RollCopy.js +276 -215
  21. package/lib/Symbol.d.ts +70 -37
  22. package/lib/Symbol.js +2 -27
  23. package/lib/TrackCalibration.d.ts +33 -0
  24. package/lib/TrackCalibration.js +14 -0
  25. package/lib/TrackerBar.d.ts +52 -5
  26. package/lib/TrackerBar.js +70 -49
  27. package/lib/Version.d.ts +32 -33
  28. package/lib/Version.js +3 -209
  29. package/lib/alignFeatures.d.ts +3 -2
  30. package/lib/alignFeatures.js +5 -4
  31. package/lib/asJsonLd.d.ts +1 -1
  32. package/lib/asJsonLd.js +13 -26
  33. package/lib/importJsonLd.d.ts +0 -1
  34. package/lib/importJsonLd.js +9 -72
  35. package/lib/index.d.ts +7 -3
  36. package/lib/index.js +7 -3
  37. package/lib/schema.json +1739 -0
  38. package/lib/spec/context.json +125 -144
  39. package/lib/systems/welteT100.d.ts +55 -0
  40. package/lib/systems/welteT100.js +191 -0
  41. package/lib/utils.d.ts +29 -0
  42. package/lib/validate.d.ts +3 -0
  43. package/lib/validate.js +10 -0
  44. package/package.json +41 -8
  45. package/lib/Condition.d.ts +0 -10
  46. package/lib/EditorialAssumption.d.ts +0 -67
  47. package/lib/EditorialAssumption.js +0 -26
  48. package/lib/Measurement.d.ts +0 -9
  49. package/lib/PlaceTimeConversion.d.ts +0 -65
  50. package/lib/PlaceTimeConversion.js +0 -175
  51. package/lib/RollEvent.d.ts +0 -76
  52. package/lib/RollEvent.js +0 -3
  53. package/lib/Stage.d.ts +0 -37
  54. package/lib/Stage.js +0 -165
  55. package/lib/Transcription.d.ts +0 -7
  56. package/lib/Transcription.js +0 -9
  57. package/lib/WithId.d.ts +0 -3
  58. package/lib/WithId.js +0 -1
  59. package/lib/alignRolls.d.ts +0 -7
  60. package/lib/alignRolls.js +0 -49
  61. package/lib/alignSymbols.d.ts +0 -7
  62. package/lib/alignSymbols.js +0 -49
  63. package/lib/aton/AtonParser.test.d.ts +0 -1
  64. package/lib/aton/AtonParser.test.js +0 -16
  65. package/lib/build-schema.cjs +0 -113
  66. /package/lib/{Condition.js → ReproducingSystem.js} +0 -0
  67. /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 { KinematicConversion } from "./PlaceTimeConversion";
7
- import { WelteT100 } from "./TrackerBar";
8
- const applyShift = (shift, to) => {
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 = (stretch, to) => {
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
- export class RollCopy {
29
- constructor() {
30
- Object.defineProperty(this, "type", {
31
- enumerable: true,
32
- configurable: true,
33
- writable: true,
34
- value: 'RollCopy'
35
- });
36
- Object.defineProperty(this, "id", {
37
- enumerable: true,
38
- configurable: true,
39
- writable: true,
40
- value: v4()
41
- });
42
- Object.defineProperty(this, "measurements", {
43
- enumerable: true,
44
- configurable: true,
45
- writable: true,
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
- export function asSymbols(features) {
115
- return features.map((feature) => {
116
- return {
117
- id: `symbol_${v4()}`,
118
- ...new WelteT100().meaningOf(feature.vertical.from),
119
- carriers: [assign('carrierAssignment', feature)]
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
- export function readFromStanfordAton(atonString, adjustByRewind = true, shift = 0) {
124
- function pixelsToMillimeters(pixels, dpi) {
125
- return pixels / dpi * 25.4;
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 holeSeparation = parseFloat(json.ROLLINFO.HOLE_SEPARATION.replace('px'));
132
- const hardMarginBass = parseFloat(json.ROLLINFO.HARD_MARGIN_BASS.replace('px'));
133
- const hardMarginTreble = parseFloat(json.ROLLINFO.HARD_MARGIN_TREBLE.replace('px'));
134
- const dpi = parseFloat(json.ROLLINFO.LENGTH_DPI.replace('ppi'));
135
- const rollWidth = parseFloat(json.ROLLINFO.ROLL_WIDTH.replace('px')) / dpi * 25.4;
136
- const rollHeight = parseFloat(json.ROLLINFO.IMAGE_LENGTH.replace('px')) / dpi * 25.4;
137
- let averagePunchDiameter = -1;
138
- const lastHole = +holes[holes.length - 1].TRACKER_HOLE;
139
- const rewindShift = adjustByRewind ? 91 - lastHole : shift;
140
- const copy = new RollCopy();
141
- copy.measurements = {
142
- dimensions: {
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
- let circularPunches = 0;
167
- const features = [];
168
- for (let i = 0; i < holes.length; i++) {
169
- const hole = holes[i];
170
- const circularity = +hole.CIRCULARITY.replace('px', '');
171
- if (circularity > 0.95) {
172
- const perimeterInMM = pixelsToMillimeters(+hole.PERIMETER.replace('px', ''), dpi);
173
- averagePunchDiameter += perimeterInMM;
174
- circularPunches += 1;
175
- }
176
- if (!hole.NOTE_ATTACK || !hole.OFF_TIME)
177
- continue;
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
- annotates,
210
+ ...(stanford && {
211
+ depiction: stanford.depictionOf(column, attack, columnWidth, release - attack)
212
+ }),
188
213
  vertical: {
189
- from: trackerHole,
214
+ from: +hole.TRACKER_HOLE + shift,
190
215
  unit: 'track'
191
216
  },
192
217
  horizontal: {
193
218
  unit: 'mm',
194
- from: pixelsToMillimeters(noteAttack, dpi),
195
- to: pixelsToMillimeters(offset, dpi)
219
+ from: millimeters(attack, dpi),
220
+ to: millimeters(release, dpi)
196
221
  }
197
222
  };
198
- features.push(feature);
199
- }
200
- averagePunchDiameter /= circularPunches;
201
- averagePunchDiameter /= Math.PI;
202
- copy.scan = `https://stacks.stanford.edu/image/iiif/${druid}%2F${druid}_0001/`;
203
- copy.features = features;
204
- return copy;
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
- * Spencer Chase's rolls seem to be scanned at a roll speed of
208
- * 83 (=8.3 feet per minute).
257
+ * How a MIDI key number in one of Spencer Chase's roll files names a
258
+ * tracker bar track.
209
259
  *
210
- * @param midiBuffer
211
- * @param conversion
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 function readFromSpencerMIDI(midiBuffer, conversion = new KinematicConversion(8.3)) {
214
- const midi = read(midiBuffer);
215
- const features = [];
216
- const copy = new RollCopy();
217
- const spans = asSpans(midi);
218
- for (const span of spans) {
219
- const left = conversion.timeToPlace(span.onsetMs / 1000) * 10;
220
- const right = conversion.timeToPlace(span.offsetMs / 1000) * 10;
221
- const horizontalDimension = {
222
- from: left,
223
- to: right,
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
- const feature = {
237
- vertical: {
238
- from: trackerHole,
239
- unit: 'track'
240
- },
241
- horizontal: horizontalDimension,
242
- id: v4()
243
- };
244
- features.push(feature);
245
- }
246
- copy.features = features;
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
- for (const event of features) {
251
- event.vertical.from += amount;
252
- try {
253
- const newEvent = new WelteT100().meaningOf(event.vertical.from);
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 flat(symbol.carriers)) {
326
+ for (const feature of idsOf(symbol.carriers)) {
266
327
  for (const copy of sources) {
267
- if (copy.features.includes(feature)) {
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 { EditorialAssumption } from "./EditorialAssumption";
2
- import { HorizontalSpan, RollFeature, VerticalSpan } from "./Feature";
3
- import { WithId } from "./WithId";
1
+ import { ReferenceAssumption, ValueAssumption } from "./Assumption";
2
+ import { WithId } from "./utils";
4
3
  /**
5
- * @todo Should this be called Transcription?
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
- carriers: CarrierAssignment[];
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
- accelerating?: boolean;
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
- * This denotes perforations that are covered by an editor.
27
- * The covered perforation is not considered to be part
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 Cover extends Symbol<'cover'> {
84
+ export interface Text extends Symbol<'text'> {
31
85
  /**
32
- * This property can be used to indicate e.g. the
33
- * color or material of the cover.
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
- * This type can be used to indicate stamps like e. g. the
50
- * "controlliert" stamp in the beginning of rolls or the
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 interface Stamp extends Text<'stamp'> {
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;