linked-rolls 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +259 -24
  20. package/lib/RollCopy.js +232 -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,266 @@ 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
+ const millimeters = (pixels, dpi) => pixels / dpi * 25.4;
104
+ const median = (values) => {
105
+ const sorted = [...values].sort((a, b) => a - b);
106
+ const middle = Math.floor(sorted.length / 2);
107
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
108
+ };
109
+ const mostFrequent = (values) => {
110
+ const counts = values.reduce((acc, value) => acc.set(value, (acc.get(value) || 0) + 1), new Map());
111
+ return [...counts].sort(([, a], [, b]) => b - a)[0]?.[0];
112
+ };
113
+ /**
114
+ * Only the first hole of a chain carries an attack and an off time;
115
+ * the rest continue it. The rewind perforation carries neither, and it
116
+ * is the last thing punched on the roll, so whatever the holes past the
117
+ * final musical attack sit on is the rewind track.
118
+ */
119
+ const rewindTrackIn = (holes) => {
120
+ const lastMusical = holes.findLastIndex(hole => hole.NOTE_ATTACK);
121
+ const trailing = holes.slice(lastMusical + 1).map(hole => +hole.TRACKER_HOLE);
122
+ return mostFrequent(trailing);
123
+ };
124
+ /**
125
+ * The phase of the tracker grid within the image. The analysis file
126
+ * usually states it; where it does not, the holes themselves give it away,
127
+ * since each sits close to the centre of its column.
128
+ */
129
+ const gridOffsetOf = (holes, separation, stated) => {
130
+ if (stated !== undefined)
131
+ return px(stated);
132
+ return median(holes.map(hole => px(hole.CENTROID_COL) - +hole.TRACKER_HOLE * separation));
133
+ };
134
+ const punchDiameterOf = (holes, dpi) => {
135
+ const circular = holes
136
+ .filter(hole => px(hole.CIRCULARITY) > 0.95)
137
+ .map(hole => millimeters(px(hole.PERIMETER), dpi) / Math.PI);
138
+ if (!circular.length)
139
+ return undefined;
140
+ return circular.reduce((sum, diameter) => sum + diameter, 0) / circular.length;
141
+ };
142
+ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100 } = {}) {
127
143
  const parser = new AtonParser();
128
144
  const json = parser.parse(atonString);
129
145
  const holes = json.ROLLINFO.HOLES.HOLE;
130
146
  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
- }
147
+ const separation = px(json.ROLLINFO.HOLE_SEPARATION);
148
+ const dpi = parseFloat(json.ROLLINFO.LENGTH_DPI);
149
+ const rewindTrack = rewindTrackIn(holes);
150
+ const shift = trackShift
151
+ ?? (rewindTrack === undefined ? 0 : bar.rewindTrack - rewindTrack);
152
+ const calibration = {
153
+ unit: 'px',
154
+ offset: gridOffsetOf(holes, separation, json.ROLLINFO.HOLE_OFFSET),
155
+ separation,
156
+ shift
165
157
  };
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 = {
158
+ const punchDiameter = punchDiameterOf(holes, dpi);
159
+ const features = holes
160
+ .filter(hole => hole.NOTE_ATTACK && hole.OFF_TIME)
161
+ .map((hole) => {
162
+ const attack = px(hole.NOTE_ATTACK);
163
+ const release = px(hole.OFF_TIME);
164
+ const column = px(hole.ORIGIN_COL);
165
+ const columnWidth = px(hole.WIDTH_COL);
166
+ return {
167
+ type: 'Hole',
186
168
  id: v4(),
187
- annotates,
169
+ depiction: `https://stacks.stanford.edu/image/iiif/${druid}/${druid}_0001/${column},${attack},${columnWidth},${release - attack}/128,/270/default.jpg`,
188
170
  vertical: {
189
- from: trackerHole,
171
+ from: +hole.TRACKER_HOLE + shift,
190
172
  unit: 'track'
191
173
  },
192
174
  horizontal: {
193
175
  unit: 'mm',
194
- from: pixelsToMillimeters(noteAttack, dpi),
195
- to: pixelsToMillimeters(offset, dpi)
176
+ from: millimeters(attack, dpi),
177
+ to: millimeters(release, dpi)
196
178
  }
197
179
  };
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;
180
+ });
181
+ return {
182
+ type: 'RollCopy',
183
+ id: v4(),
184
+ ops: [],
185
+ conditions: [],
186
+ location: '',
187
+ modifications: [],
188
+ scan: `https://stacks.stanford.edu/image/iiif/${druid}%2F${druid}_0001/`,
189
+ measurements: {
190
+ dimensions: {
191
+ width: millimeters(px(json.ROLLINFO.ROLL_WIDTH), dpi),
192
+ height: millimeters(px(json.ROLLINFO.IMAGE_LENGTH), dpi),
193
+ unit: 'mm'
194
+ },
195
+ ...(punchDiameter !== undefined && {
196
+ punchDiameter: { value: punchDiameter, unit: 'mm' }
197
+ }),
198
+ holeSeparation: {
199
+ value: separation,
200
+ unit: 'px'
201
+ },
202
+ margins: {
203
+ treble: px(json.ROLLINFO.HARD_MARGIN_TREBLE),
204
+ bass: px(json.ROLLINFO.HARD_MARGIN_BASS),
205
+ unit: 'px'
206
+ },
207
+ trackCalibration: calibration
208
+ },
209
+ features
210
+ };
205
211
  }
206
212
  /**
207
- * Spencer Chase's rolls seem to be scanned at a roll speed of
208
- * 83 (=8.3 feet per minute).
213
+ * How a MIDI key number in one of Spencer Chase's roll files names a
214
+ * tracker bar track.
209
215
  *
210
- * @param midiBuffer
211
- * @param conversion
216
+ * The note block follows the obvious rule, `pitch - 13`, which puts
217
+ * track 11 on MIDI 24 as the T100 compass requires. The bass expression
218
+ * block does not: it reads two tracks high, and subtracting two is what
219
+ * has made these files come out right so far.
220
+ *
221
+ * The boundary between the two rules is unresolved. Taken literally the
222
+ * rules leave tracks 8 and 9 unreachable and jump from track 7 to track 10,
223
+ * which no lateral offset can produce, so at least one of them is
224
+ * approximate. Settling it needs a Spencer file whose expression holes
225
+ * can be checked against the roll, hence the option to override.
212
226
  */
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,
227
+ export const spencerTrackOf = (pitch) => {
228
+ const track = pitch - 13;
229
+ return track < 10 ? track - 2 : track;
230
+ };
231
+ const MM_PER_FOOT = 304.8;
232
+ /**
233
+ * Spencer Chase's rolls seem to be scanned at a roll speed of
234
+ * 83 (=8.3 feet per minute). A scanner feeds the paper at one
235
+ * speed, so time in his files is proportional to place.
236
+ */
237
+ export const SPENCER_FEET_PER_MINUTE = 8.3;
238
+ /** Place on the roll in mm after `seconds` at a constant `feetPerMinute`. */
239
+ export const atConstantSpeed = (feetPerMinute) => (seconds) => feetPerMinute * MM_PER_FOOT / 60 * seconds;
240
+ export function readFromSpencerMIDI(midiBuffer, placeAt = atConstantSpeed(SPENCER_FEET_PER_MINUTE), trackOf = spencerTrackOf) {
241
+ const features = asSpans(read(midiBuffer))
242
+ .filter(span => span.type === 'note')
243
+ .map((span) => ({
244
+ type: 'Hole',
245
+ id: v4(),
246
+ vertical: {
247
+ from: trackOf(span.pitch),
248
+ unit: 'track'
249
+ },
250
+ horizontal: {
251
+ from: placeAt(span.onsetMs / 1000),
252
+ to: placeAt(span.offsetMs / 1000),
224
253
  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
254
  }
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;
255
+ }));
256
+ return {
257
+ type: 'RollCopy',
258
+ id: v4(),
259
+ ops: [],
260
+ conditions: [],
261
+ location: '',
262
+ measurements: {},
263
+ modifications: [],
264
+ features
265
+ };
248
266
  }
267
+ /**
268
+ * Moves features across the tracker bar. What the new position means is
269
+ * left to the tracker bar to say, since meaning belongs to the symbols
270
+ * read off a copy rather than to the holes themselves.
271
+ */
249
272
  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
- }
273
+ features.forEach(feature => {
274
+ feature.vertical.from += amount;
275
+ if (feature.vertical.to !== undefined) {
276
+ feature.vertical.to += amount;
257
277
  }
258
- catch (e) {
259
- console.error(e);
260
- }
261
- }
278
+ });
262
279
  }
263
280
  export const findCopiesCarrying = (sources, symbol) => {
264
281
  const result = new Set();
265
- for (const feature of flat(symbol.carriers)) {
282
+ for (const feature of idsOf(symbol.carriers)) {
266
283
  for (const copy of sources) {
267
- if (copy.features.includes(feature)) {
284
+ if (copy.features.findIndex(f => f.id === feature)) {
268
285
  result.add(copy.id);
269
286
  }
270
287
  }
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;
package/lib/Symbol.js CHANGED
@@ -1,29 +1,4 @@
1
- import { flat } from "./EditorialAssumption";
2
1
  export const isSymbol = (object) => {
3
- return 'carriers' in object;
4
- };
5
- export const dimensionOf = (symbol) => {
6
- if (symbol.carriers.length === 0) {
7
- return {
8
- horizontal: { unit: 'mm', from: 0, to: 0 },
9
- vertical: { unit: 'track', from: 0, to: 0 }
10
- };
11
- }
12
- const horizontalFrom = flat(symbol.carriers)
13
- .reduce((hAcc, h) => hAcc + h.horizontal.from, 0) / symbol.carriers.length;
14
- const horizontalTo = flat(symbol.carriers)
15
- .reduce((hAcc, h) => hAcc + h.horizontal.to, 0) / symbol.carriers.length;
16
- const verticalFrom = flat(symbol.carriers)
17
- .reduce((vAcc, v) => vAcc + v.vertical.from, 0) / symbol.carriers.length;
18
- const definedVerticalTo = flat(symbol.carriers)
19
- .filter(v => v.vertical.to !== undefined);
20
- let verticalTo = undefined;
21
- if (definedVerticalTo.length > 0) {
22
- verticalTo = definedVerticalTo
23
- .reduce((vAcc, v) => vAcc + v.vertical.to, 0) / definedVerticalTo.length;
24
- }
25
- return {
26
- horizontal: { unit: 'mm', from: horizontalFrom, to: horizontalTo },
27
- vertical: { unit: 'track', from: verticalFrom, to: verticalTo }
28
- };
2
+ return ('type' in object
3
+ && (object.type === 'note' || object.type === 'expression' || object.type === 'text'));
29
4
  };