linked-rolls 0.16.0 → 0.18.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/Feature.d.ts +11 -1
- package/lib/Feature.js +5 -0
- package/lib/editionOps.d.ts +11 -1
- package/lib/editionOps.js +47 -12
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/readers/phillipsEroll.d.ts +62 -0
- package/lib/readers/phillipsEroll.js +164 -0
- package/lib/schema.json +5 -1
- package/package.json +1 -1
package/lib/Feature.d.ts
CHANGED
|
@@ -173,7 +173,7 @@ export interface GluedOn extends RollFeature<'GluedOn', typeof conditions.GluedO
|
|
|
173
173
|
* Nested features do not need to be positioned explicitly.
|
|
174
174
|
* @see crm:P56 bears feature
|
|
175
175
|
*/
|
|
176
|
-
features?:
|
|
176
|
+
features?: NestedFeature[];
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
179
|
* The union of all physical feature types that can appear on a roll:
|
|
@@ -181,4 +181,14 @@ export interface GluedOn extends RollFeature<'GluedOn', typeof conditions.GluedO
|
|
|
181
181
|
* and glued-on patches (paper, tape).
|
|
182
182
|
*/
|
|
183
183
|
export type AnyFeature = Hole | Writing | Mark | GluedOn;
|
|
184
|
+
/**
|
|
185
|
+
* A feature borne by another feature. It states no place of its own,
|
|
186
|
+
* the feature bearing it standing in one.
|
|
187
|
+
*/
|
|
188
|
+
export type NestedFeature = PartialBy<AnyFeature, 'horizontal' | 'vertical'>;
|
|
184
189
|
export declare const isRollFeature: (obj: object) => obj is AnyFeature;
|
|
190
|
+
export declare const isGluedOn: <T extends NestedFeature>(feature: T) => feature is T & GluedOn;
|
|
191
|
+
/** The features a feature bears: a patch those stated as parts of it, any other feature none. */
|
|
192
|
+
export declare const featuresBorneBy: (feature: NestedFeature) => NestedFeature[];
|
|
193
|
+
/** The feature together with everything it bears, as deep as a patch on a patch goes. */
|
|
194
|
+
export declare const withBorneFeatures: (feature: NestedFeature) => NestedFeature[];
|
package/lib/Feature.js
CHANGED
|
@@ -9,3 +9,8 @@ export const writingMethods = ['Print', 'Handwriting', 'Stamp'];
|
|
|
9
9
|
export const isRollFeature = (obj) => {
|
|
10
10
|
return 'type' in obj && featureTypes.includes(obj.type);
|
|
11
11
|
};
|
|
12
|
+
export const isGluedOn = (feature) => feature.type === 'GluedOn';
|
|
13
|
+
/** The features a feature bears: a patch those stated as parts of it, any other feature none. */
|
|
14
|
+
export const featuresBorneBy = (feature) => isGluedOn(feature) ? feature.features ?? [] : [];
|
|
15
|
+
/** The feature together with everything it bears, as deep as a patch on a patch goes. */
|
|
16
|
+
export const withBorneFeatures = (feature) => [feature, ...featuresBorneBy(feature).flatMap(withBorneFeatures)];
|
package/lib/editionOps.d.ts
CHANGED
|
@@ -50,7 +50,12 @@ export declare const addGeneralCondition: (copyId: string, condition: ObjectAssu
|
|
|
50
50
|
export declare const stateFeatureCondition: (copyId: string, featureId: string, condition: FeatureConditionAssignment) => EditionOp;
|
|
51
51
|
/** The symbols of the versions that no other copy carries. */
|
|
52
52
|
export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Takes the features off the copy, and out of the versions with what
|
|
55
|
+
* only they carried. A feature may be named wherever the copy bears it:
|
|
56
|
+
* a patch goes with everything glued onto it, and a feature of a patch
|
|
57
|
+
* may be taken back on its own, the patch staying where it is.
|
|
58
|
+
*/
|
|
54
59
|
export declare const removeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
|
|
55
60
|
/**
|
|
56
61
|
* Takes the copy out of the edition together with the symbols only it
|
|
@@ -78,6 +83,11 @@ export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeOb
|
|
|
78
83
|
* whatever else named them, a modification or a comprehension, names
|
|
79
84
|
* the merged one in their stead.
|
|
80
85
|
*
|
|
86
|
+
* Only the features the copy bears itself are merged. A feature of a
|
|
87
|
+
* patch states no place along the roll of its own, so there is nothing
|
|
88
|
+
* for a merge to span, and an id naming one is passed over as an id the
|
|
89
|
+
* copy does not bear is.
|
|
90
|
+
*
|
|
81
91
|
* Throws where the features cannot stand for one; `mergeObstacle`
|
|
82
92
|
* says beforehand whether they can.
|
|
83
93
|
*/
|
package/lib/editionOps.js
CHANGED
|
@@ -7,7 +7,7 @@ import { collationToleranceOf, insertedBy } from "./Version";
|
|
|
7
7
|
import { asSymbols } from "./RollCopy";
|
|
8
8
|
import { applyShift, applyScale, revertShift, revertScale } from "./alignment";
|
|
9
9
|
import { assignReference, idOf } from "./Assumption";
|
|
10
|
-
import { conditions as conditionsAllowed } from "./Feature";
|
|
10
|
+
import { conditions as conditionsAllowed, featuresBorneBy, isGluedOn, withBorneFeatures } from "./Feature";
|
|
11
11
|
import { distance, mm, subtract } from "./Quantity";
|
|
12
12
|
const noChange = () => undefined;
|
|
13
13
|
const onCopy = (copyId, op) => draft => {
|
|
@@ -147,7 +147,8 @@ export const stateFeatureCondition = (copyId, featureId, condition) => onFeature
|
|
|
147
147
|
}
|
|
148
148
|
feature.condition = condition;
|
|
149
149
|
});
|
|
150
|
-
|
|
150
|
+
/** The ids of every feature the copy bears, those a patch bears among them. */
|
|
151
|
+
const featureIdsOf = (copy) => new Set(copy.features.flatMap(withBorneFeatures).map(feature => feature.id));
|
|
151
152
|
/**
|
|
152
153
|
* A symbol every carrier of which lies among the features loses its
|
|
153
154
|
* evidence with them. A symbol without carriers, such as a label,
|
|
@@ -282,11 +283,33 @@ const forgetFeatures = (draft, features) => {
|
|
|
282
283
|
version.edits = edits[i];
|
|
283
284
|
});
|
|
284
285
|
};
|
|
285
|
-
/**
|
|
286
|
+
/**
|
|
287
|
+
* The ids of the features named and of everything they bear: a feature
|
|
288
|
+
* of a patch stands nowhere once the patch is gone.
|
|
289
|
+
*/
|
|
290
|
+
const goneWith = (features, named) => features.flatMap(feature => named.has(feature.id)
|
|
291
|
+
? withBorneFeatures(feature).map(borne => borne.id)
|
|
292
|
+
: goneWith(featuresBorneBy(feature), named));
|
|
293
|
+
/** The feature with the named ones gone from what it bears, or the very same one where it bears none of them. */
|
|
294
|
+
const withoutBorne = (feature, named) => {
|
|
295
|
+
if (!isGluedOn(feature) || !feature.features)
|
|
296
|
+
return feature;
|
|
297
|
+
const borne = withoutFeatures(feature.features, named);
|
|
298
|
+
return borne === feature.features ? feature : { ...feature, features: borne };
|
|
299
|
+
};
|
|
300
|
+
/** The features without those named, wherever they lie, and without whatever those bore. */
|
|
301
|
+
const withoutFeatures = (features, named) => mapped(without(features, feature => named.has(feature.id)), feature => withoutBorne(feature, named));
|
|
302
|
+
/**
|
|
303
|
+
* Takes the features off the copy, and out of the versions with what
|
|
304
|
+
* only they carried. A feature may be named wherever the copy bears it:
|
|
305
|
+
* a patch goes with everything glued onto it, and a feature of a patch
|
|
306
|
+
* may be taken back on its own, the patch staying where it is.
|
|
307
|
+
*/
|
|
286
308
|
export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
309
|
+
const named = new Set(featureIds);
|
|
310
|
+
const features = stateOf(copy.features);
|
|
311
|
+
copy.features = withoutFeatures(features, named);
|
|
312
|
+
forgetFeatures(draft, new Set(goneWith(features, named)));
|
|
290
313
|
});
|
|
291
314
|
/**
|
|
292
315
|
* Takes the copy out of the edition together with the symbols only it
|
|
@@ -311,10 +334,17 @@ const sayTheSame = (a, b) => {
|
|
|
311
334
|
keys.delete('id');
|
|
312
335
|
return [...keys].every(key => sayTheSame(a[key], b[key]));
|
|
313
336
|
};
|
|
314
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* What a feature states beyond its identity, its place along the roll,
|
|
339
|
+
* its depiction and its condition. What a patch bears counts by
|
|
340
|
+
* identity: two patches bear the same only where they bear the very
|
|
341
|
+
* same features, so that no merge takes a feature of a patch away.
|
|
342
|
+
*/
|
|
315
343
|
const nature = (feature) => {
|
|
316
344
|
const { id, horizontal, depiction, condition, ...rest } = feature;
|
|
317
|
-
return
|
|
345
|
+
return isGluedOn(feature) && feature.features
|
|
346
|
+
? { ...rest, features: feature.features.map(borne => borne.id) }
|
|
347
|
+
: rest;
|
|
318
348
|
};
|
|
319
349
|
const conditionsOf = (features) => features.flatMap(feature => feature.condition ? [feature.condition] : []);
|
|
320
350
|
/**
|
|
@@ -393,20 +423,25 @@ const carryOver = (draft, replaced, mergedId) => {
|
|
|
393
423
|
* whatever else named them, a modification or a comprehension, names
|
|
394
424
|
* the merged one in their stead.
|
|
395
425
|
*
|
|
426
|
+
* Only the features the copy bears itself are merged. A feature of a
|
|
427
|
+
* patch states no place along the roll of its own, so there is nothing
|
|
428
|
+
* for a merge to span, and an id naming one is passed over as an id the
|
|
429
|
+
* copy does not bear is.
|
|
430
|
+
*
|
|
396
431
|
* Throws where the features cannot stand for one; `mergeObstacle`
|
|
397
432
|
* says beforehand whether they can.
|
|
398
433
|
*/
|
|
399
434
|
export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
|
|
400
|
-
const
|
|
401
|
-
const isReplaced = (feature) => replaced.has(feature.id);
|
|
435
|
+
const named = new Set(featureIds);
|
|
402
436
|
const features = stateOf(copy.features);
|
|
403
|
-
const toMerge = features.filter(
|
|
437
|
+
const toMerge = features.filter(feature => named.has(feature.id));
|
|
404
438
|
const obstacle = mergeObstacle(toMerge);
|
|
405
439
|
if (obstacle) {
|
|
406
440
|
throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
|
|
407
441
|
}
|
|
442
|
+
const replaced = new Set(toMerge.map(feature => feature.id));
|
|
408
443
|
const merged = mergedFrom(toMerge);
|
|
409
|
-
copy.features = standingFor(features,
|
|
444
|
+
copy.features = standingFor(features, feature => replaced.has(feature.id), merged);
|
|
410
445
|
carryOver(draft, replaced, merged.id);
|
|
411
446
|
renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), merged.id));
|
|
412
447
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -29,4 +29,5 @@ export * from './asJsonLd';
|
|
|
29
29
|
export * from './importJsonLd';
|
|
30
30
|
export * from './migrate';
|
|
31
31
|
export { readFromStanfordAton, type StanfordAtonOptions } from './readers/stanfordAton';
|
|
32
|
+
export { readFromPhillipsEroll, CONTROL_OFFSET, phillipsSystems, type PhillipsErollOptions } from './readers/phillipsEroll';
|
|
32
33
|
export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH, type SpencerBarOptions } from './readers/spencerBar';
|
package/lib/index.js
CHANGED
|
@@ -29,4 +29,5 @@ export * from './asJsonLd';
|
|
|
29
29
|
export * from './importJsonLd';
|
|
30
30
|
export * from './migrate';
|
|
31
31
|
export { readFromStanfordAton } from './readers/stanfordAton';
|
|
32
|
+
export { readFromPhillipsEroll, CONTROL_OFFSET, phillipsSystems } from './readers/phillipsEroll';
|
|
32
33
|
export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH } from './readers/spencerBar';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { RollCopy } from "../RollCopy";
|
|
2
|
+
import { TrackerBar } from "../TrackerBar";
|
|
3
|
+
import { Millimeters, Seconds } from "../Quantity";
|
|
4
|
+
/**
|
|
5
|
+
* Peter Phillips's "e-roll" file, the unprocessed output of his
|
|
6
|
+
* pneumatic roll reader (thesis pp. 182–196): the roll runs over a
|
|
7
|
+
* tracker bar and a switch on every position writes a MIDI note as its
|
|
8
|
+
* perforation passes. Every position is there, expression included, all
|
|
9
|
+
* at one velocity. His other product, the standard MIDI file, has the
|
|
10
|
+
* expression decoded into velocities already and cannot be read as a
|
|
11
|
+
* roll.
|
|
12
|
+
*
|
|
13
|
+
* Two things follow from a reader rather than a scanner. The events are
|
|
14
|
+
* elapsed time, not paper, and the take-up spool accelerates the paper
|
|
15
|
+
* as it fills, so `placeAt` has to put the time back onto the paper.
|
|
16
|
+
* And a switch stays open longer than its perforation is long, so a
|
|
17
|
+
* hole read here runs past the punched one.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* He numbers his files by two rules at once, which his free samples
|
|
21
|
+
* show and which follow from what the file is for. A note carries the
|
|
22
|
+
* MIDI number it sounds, so that the file plays on a MIDI instrument
|
|
23
|
+
* without translation. An expression position, having no pitch to
|
|
24
|
+
* carry, is numbered by its place on the bar plus a fixed offset: on a
|
|
25
|
+
* red e-roll the sustain commands sit at 107 and 108 for positions 93
|
|
26
|
+
* and 94, and the soft pedal at 21 and 22 for positions 7 and 8, so the
|
|
27
|
+
* offset is fourteen.
|
|
28
|
+
*
|
|
29
|
+
* The two rules collide on one number, the lowest note of the T-100
|
|
30
|
+
* sharing 24 with the tenth position. Notes win there, the note block
|
|
31
|
+
* being the larger claim; none of his samples uses the number at all.
|
|
32
|
+
*/
|
|
33
|
+
export declare const CONTROL_OFFSET: Readonly<Record<string, number>>;
|
|
34
|
+
export interface PhillipsErollOptions {
|
|
35
|
+
/**
|
|
36
|
+
* The bar the file numbers its positions by, which is the bar of
|
|
37
|
+
* the roll he read.
|
|
38
|
+
*/
|
|
39
|
+
system?: TrackerBar;
|
|
40
|
+
/** The edition's bar, onto which the positions are put. */
|
|
41
|
+
bar?: TrackerBar;
|
|
42
|
+
/**
|
|
43
|
+
* How far above its position an expression number sits. Defaults to
|
|
44
|
+
* what his samples show for that bar.
|
|
45
|
+
*/
|
|
46
|
+
controlOffset?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Where on the paper the roll had run after so many seconds. The
|
|
49
|
+
* default is the constant speed the system states, which ignores
|
|
50
|
+
* the take-up spool; pass `paperAt` of welte-t100-emulator to
|
|
51
|
+
* account for it.
|
|
52
|
+
*/
|
|
53
|
+
placeAt?: (elapsed: Seconds) => Millimeters;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reads one of his e-roll files as a copy of the roll, in millimetres
|
|
57
|
+
* of paper. A position the edition's bar does not read is left out, as
|
|
58
|
+
* the bar would leave it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readFromPhillipsEroll(buffer: ArrayBuffer, { system, bar, controlOffset, placeAt }?: PhillipsErollOptions): RollCopy;
|
|
61
|
+
/** The bars his files are known to be numbered by. */
|
|
62
|
+
export declare const phillipsSystems: readonly TrackerBar[];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { read } from "midifile-ts";
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
import { assignObject } from "../Assumption";
|
|
4
|
+
import { systemOf, translationBetween } from "../TrackerBar";
|
|
5
|
+
import { welteT100 } from "../systems/welteT100/bar";
|
|
6
|
+
import { welteLicensee } from "../systems/welteLicensee/bar";
|
|
7
|
+
import { inMetersPerMinute, mm, seconds, track } from "../Quantity";
|
|
8
|
+
/**
|
|
9
|
+
* Peter Phillips's "e-roll" file, the unprocessed output of his
|
|
10
|
+
* pneumatic roll reader (thesis pp. 182–196): the roll runs over a
|
|
11
|
+
* tracker bar and a switch on every position writes a MIDI note as its
|
|
12
|
+
* perforation passes. Every position is there, expression included, all
|
|
13
|
+
* at one velocity. His other product, the standard MIDI file, has the
|
|
14
|
+
* expression decoded into velocities already and cannot be read as a
|
|
15
|
+
* roll.
|
|
16
|
+
*
|
|
17
|
+
* Two things follow from a reader rather than a scanner. The events are
|
|
18
|
+
* elapsed time, not paper, and the take-up spool accelerates the paper
|
|
19
|
+
* as it fills, so `placeAt` has to put the time back onto the paper.
|
|
20
|
+
* And a switch stays open longer than its perforation is long, so a
|
|
21
|
+
* hole read here runs past the punched one.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* He numbers his files by two rules at once, which his free samples
|
|
25
|
+
* show and which follow from what the file is for. A note carries the
|
|
26
|
+
* MIDI number it sounds, so that the file plays on a MIDI instrument
|
|
27
|
+
* without translation. An expression position, having no pitch to
|
|
28
|
+
* carry, is numbered by its place on the bar plus a fixed offset: on a
|
|
29
|
+
* red e-roll the sustain commands sit at 107 and 108 for positions 93
|
|
30
|
+
* and 94, and the soft pedal at 21 and 22 for positions 7 and 8, so the
|
|
31
|
+
* offset is fourteen.
|
|
32
|
+
*
|
|
33
|
+
* The two rules collide on one number, the lowest note of the T-100
|
|
34
|
+
* sharing 24 with the tenth position. Notes win there, the note block
|
|
35
|
+
* being the larger claim; none of his samples uses the number at all.
|
|
36
|
+
*/
|
|
37
|
+
export const CONTROL_OFFSET = {
|
|
38
|
+
'welte-t100': 14,
|
|
39
|
+
'welte-licensee': 15
|
|
40
|
+
};
|
|
41
|
+
const DEFAULT_TEMPO = 500000;
|
|
42
|
+
const isNoteOn = (event) => event.type === 'channel' && event.subtype === 'noteOn' && event.velocity > 0;
|
|
43
|
+
const isNoteOff = (event) => event.type === 'channel' && (event.subtype === 'noteOff' || (event.subtype === 'noteOn' && event.velocity === 0));
|
|
44
|
+
/** The events of every track on one clock, since a note may end on another track than it began. */
|
|
45
|
+
const onOneClock = (tracks) => tracks
|
|
46
|
+
.flatMap(events => {
|
|
47
|
+
let at = 0;
|
|
48
|
+
return events.map(event => ({ at: at += event.deltaTime, event }));
|
|
49
|
+
})
|
|
50
|
+
.sort((a, b) => a.at - b.at);
|
|
51
|
+
const temposIn = (timed) => {
|
|
52
|
+
const changes = timed.flatMap(({ at, event }) => event.type === 'meta' && event.subtype === 'setTempo'
|
|
53
|
+
? [{ at, microsecondsPerBeat: event.microsecondsPerBeat }]
|
|
54
|
+
: []);
|
|
55
|
+
return changes.length > 0 && changes[0].at === 0
|
|
56
|
+
? changes
|
|
57
|
+
: [{ at: 0, microsecondsPerBeat: DEFAULT_TEMPO }, ...changes];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Turns a tick into elapsed seconds, following the tempo changes. His
|
|
61
|
+
* files carry one tempo, but a file written by hand may carry several.
|
|
62
|
+
*/
|
|
63
|
+
const clockOf = (tempos, ticksPerBeat) => {
|
|
64
|
+
/** Microseconds elapsed at the start of each tempo, the first being zero. */
|
|
65
|
+
const elapsedBefore = tempos.map((_, index) => tempos
|
|
66
|
+
.slice(0, index)
|
|
67
|
+
.reduce((total, tempo, at) => total + (tempos[at + 1].at - tempo.at) * tempo.microsecondsPerBeat, 0));
|
|
68
|
+
return (tick) => {
|
|
69
|
+
const index = Math.max(tempos.findLastIndex(tempo => tempo.at <= tick), 0);
|
|
70
|
+
const microseconds = elapsedBefore[index] + (tick - tempos[index].at) * tempos[index].microsecondsPerBeat;
|
|
71
|
+
return seconds(microseconds / ticksPerBeat / 1000000);
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/** Pairs note-on with the next note-off of the same pitch. */
|
|
75
|
+
const notesIn = (timed) => {
|
|
76
|
+
const open = new Map();
|
|
77
|
+
const notes = [];
|
|
78
|
+
timed.forEach(({ at, event }) => {
|
|
79
|
+
if (event.type !== 'channel' || !('noteNumber' in event))
|
|
80
|
+
return;
|
|
81
|
+
const pitch = event.noteNumber;
|
|
82
|
+
if (isNoteOn(event)) {
|
|
83
|
+
open.set(pitch, [...(open.get(pitch) ?? []), at]);
|
|
84
|
+
}
|
|
85
|
+
else if (isNoteOff(event)) {
|
|
86
|
+
const [from, ...rest] = open.get(pitch) ?? [];
|
|
87
|
+
if (from === undefined)
|
|
88
|
+
return;
|
|
89
|
+
open.set(pitch, rest);
|
|
90
|
+
notes.push({ pitch, from, to: at });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return notes.sort((a, b) => a.from - b.from || a.pitch - b.pitch);
|
|
94
|
+
};
|
|
95
|
+
/** Paper run at the constant speed the system states, for a bar that states one. */
|
|
96
|
+
const atStatedSpeed = (system) => {
|
|
97
|
+
const speed = system.paperSpeed;
|
|
98
|
+
if (!speed) {
|
|
99
|
+
throw new Error(`Phillips e-roll: the ${system.name} states no paper speed, so pass placeAt to say where the paper had run`);
|
|
100
|
+
}
|
|
101
|
+
const perSecond = inMetersPerMinute(speed) * 1000 / 60;
|
|
102
|
+
return (elapsed) => mm(elapsed * perSecond);
|
|
103
|
+
};
|
|
104
|
+
const controlOffsetOf = (system, given) => {
|
|
105
|
+
const measured = given ?? CONTROL_OFFSET[system.id];
|
|
106
|
+
if (measured === undefined) {
|
|
107
|
+
throw new Error(`Phillips e-roll: no control offset is known for the ${system.name}, so pass controlOffset to say how far above its position an expression number sits`);
|
|
108
|
+
}
|
|
109
|
+
return measured;
|
|
110
|
+
};
|
|
111
|
+
/** The position each note of the bar sounds from. */
|
|
112
|
+
const positionsByPitch = (bar) => new Map(Array.from({ length: bar.trackCount }, (_, index) => track(index + 1))
|
|
113
|
+
.flatMap((position) => {
|
|
114
|
+
const meaning = bar.meaningOf(position);
|
|
115
|
+
return meaning?.type === 'note' ? [[meaning.pitch, position]] : [];
|
|
116
|
+
}));
|
|
117
|
+
/**
|
|
118
|
+
* Reads one of his e-roll files as a copy of the roll, in millimetres
|
|
119
|
+
* of paper. A position the edition's bar does not read is left out, as
|
|
120
|
+
* the bar would leave it.
|
|
121
|
+
*/
|
|
122
|
+
export function readFromPhillipsEroll(buffer, { system = welteT100, bar = welteT100, controlOffset, placeAt } = {}) {
|
|
123
|
+
const file = read(new Uint8Array(buffer));
|
|
124
|
+
const timed = onOneClock(file.tracks);
|
|
125
|
+
const elapsedAt = clockOf(temposIn(timed), file.header.ticksPerBeat);
|
|
126
|
+
const place = placeAt ?? atStatedSpeed(system);
|
|
127
|
+
const offset = controlOffsetOf(system, controlOffset);
|
|
128
|
+
const notePositions = positionsByPitch(system);
|
|
129
|
+
const onBar = translationBetween(system, bar);
|
|
130
|
+
const positionOf = (number) => notePositions.get(number) ?? track(number - offset);
|
|
131
|
+
const features = notesIn(timed).flatMap((note) => {
|
|
132
|
+
const position = onBar(positionOf(note.pitch));
|
|
133
|
+
if (position === undefined)
|
|
134
|
+
return [];
|
|
135
|
+
return [{
|
|
136
|
+
type: 'Hole',
|
|
137
|
+
id: v4(),
|
|
138
|
+
vertical: { from: position, unit: 'track' },
|
|
139
|
+
horizontal: {
|
|
140
|
+
unit: 'mm',
|
|
141
|
+
from: place(elapsedAt(note.from)),
|
|
142
|
+
to: place(elapsedAt(note.to))
|
|
143
|
+
}
|
|
144
|
+
}];
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
type: 'RollCopy',
|
|
148
|
+
id: v4(),
|
|
149
|
+
ops: [],
|
|
150
|
+
conditions: [],
|
|
151
|
+
keeper: { name: '', sameAs: [] },
|
|
152
|
+
measurements: {},
|
|
153
|
+
production: { system: systemOf(system) },
|
|
154
|
+
modifications: [],
|
|
155
|
+
features,
|
|
156
|
+
readFrom: {
|
|
157
|
+
kind: 'recording',
|
|
158
|
+
actor: assignObject({ name: 'Phillips, Peter', sameAs: [] }),
|
|
159
|
+
note: 'Read on Phillips’s pneumatic roll reader. The places are elapsed time put back onto the paper, and a hole runs as long as its switch stayed open, which is longer than the perforation.'
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** The bars his files are known to be numbered by. */
|
|
164
|
+
export const phillipsSystems = [welteT100, welteLicensee];
|
package/lib/schema.json
CHANGED
|
@@ -626,7 +626,7 @@
|
|
|
626
626
|
"features": {
|
|
627
627
|
"description": "A glued-on feature itself may carry other features. Nested features do not need to be positioned explicitly. [ontology: crm:P56 bears feature]",
|
|
628
628
|
"items": {
|
|
629
|
-
"$ref": "#/definitions/
|
|
629
|
+
"$ref": "#/definitions/NestedFeature"
|
|
630
630
|
},
|
|
631
631
|
"type": "array"
|
|
632
632
|
},
|
|
@@ -1049,6 +1049,10 @@
|
|
|
1049
1049
|
],
|
|
1050
1050
|
"type": "object"
|
|
1051
1051
|
},
|
|
1052
|
+
"NestedFeature": {
|
|
1053
|
+
"$ref": "#/definitions/PartialBy%3CAnyFeature%2C(%22horizontal%22%7C%22vertical%22)%3E",
|
|
1054
|
+
"description": "A feature borne by another feature. It states no place of its own, the feature bearing it standing in one."
|
|
1055
|
+
},
|
|
1052
1056
|
"Note": {
|
|
1053
1057
|
"description": "A note symbol, representing a single pitched musical event on the roll. The pitch is encoded via the tracker bar position (track number). [ontology: reo:Note]",
|
|
1054
1058
|
"properties": {
|
package/package.json
CHANGED