linked-rolls 0.57.0 → 0.59.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/README.md +54 -367
- package/lib/{constraints.d.ts → analysis/constraints.d.ts} +1 -1
- package/lib/{constraints.js → analysis/constraints.js} +8 -7
- package/lib/analysis/ownPaper.d.ts +26 -0
- package/lib/analysis/ownPaper.js +41 -0
- package/lib/{reservations.d.ts → analysis/reservations.d.ts} +3 -3
- package/lib/{reservations.js → analysis/reservations.js} +3 -3
- package/lib/{sigla.d.ts → analysis/sigla.d.ts} +2 -2
- package/lib/{sigla.js → analysis/sigla.js} +4 -4
- package/lib/{witnesses.d.ts → analysis/witnesses.d.ts} +2 -2
- package/lib/{witnesses.js → analysis/witnesses.js} +2 -2
- package/lib/{Collation.d.ts → collation/Collation.d.ts} +4 -4
- package/lib/{Collation.js → collation/Collation.js} +4 -4
- package/lib/{alignment.d.ts → collation/alignment.d.ts} +4 -4
- package/lib/{alignment.js → collation/alignment.js} +10 -16
- package/lib/{scatter.d.ts → collation/scatter.d.ts} +4 -4
- package/lib/{scatter.js → collation/scatter.js} +3 -3
- package/lib/{statistics.d.ts → collation/statistics.d.ts} +1 -1
- package/lib/{statistics.js → collation/statistics.js} +2 -2
- package/lib/{substitution.d.ts → collation/substitution.d.ts} +9 -24
- package/lib/{substitution.js → collation/substitution.js} +13 -37
- package/lib/emulation/Emulation.d.ts +64 -0
- package/lib/emulation/Emulation.js +282 -0
- package/lib/emulation/negotiation.d.ts +14 -0
- package/lib/emulation/negotiation.js +20 -0
- package/lib/index.d.ts +43 -35
- package/lib/index.js +36 -35
- package/lib/{asJsonLd.d.ts → io/asJsonLd.d.ts} +1 -1
- package/lib/{asJsonLd.js → io/asJsonLd.js} +3 -3
- package/lib/{context.d.ts → io/context.d.ts} +4 -4
- package/lib/{context.js → io/context.js} +4 -4
- package/lib/{importJsonLd.d.ts → io/importJsonLd.d.ts} +1 -1
- package/lib/io/importJsonLd.js +128 -0
- package/lib/{migrate.js → io/migrate.js} +59 -55
- package/lib/{Agent.d.ts → model/Agent.d.ts} +1 -1
- package/lib/{Assumption.d.ts → model/Assumption.d.ts} +1 -1
- package/lib/{Edit.d.ts → model/Edit.d.ts} +1 -1
- package/lib/{Edition.d.ts → model/Edition.d.ts} +2 -2
- package/lib/{Feature.d.ts → model/Feature.d.ts} +1 -1
- package/lib/{FeatureSource.d.ts → model/FeatureSource.d.ts} +1 -1
- package/lib/{Perforator.d.ts → model/Perforator.d.ts} +1 -1
- package/lib/{RollCopy.d.ts → model/RollCopy.d.ts} +3 -10
- package/lib/{RollCopy.js → model/RollCopy.js} +3 -5
- package/lib/{Symbol.d.ts → model/Symbol.d.ts} +1 -1
- package/lib/{Version.d.ts → model/Version.d.ts} +10 -2
- package/lib/{Version.js → model/Version.js} +24 -1
- package/lib/{vocabulary.js → model/vocabulary.js} +2 -2
- package/lib/ops/beliefs.d.ts +9 -0
- package/lib/ops/beliefs.js +31 -0
- package/lib/ops/collation.d.ts +62 -0
- package/lib/ops/collation.js +228 -0
- package/lib/ops/commands.d.ts +9 -0
- package/lib/ops/commands.js +29 -0
- package/lib/ops/copies.d.ts +74 -0
- package/lib/ops/copies.js +135 -0
- package/lib/ops/draft.d.ts +59 -0
- package/lib/ops/draft.js +81 -0
- package/lib/ops/edits.d.ts +10 -0
- package/lib/ops/edits.js +82 -0
- package/lib/ops/features.d.ts +84 -0
- package/lib/ops/features.js +296 -0
- package/lib/ops/forget.d.ts +31 -0
- package/lib/ops/forget.js +110 -0
- package/lib/ops/immutable.d.ts +16 -0
- package/lib/ops/immutable.js +46 -0
- package/lib/ops/index.d.ts +13 -0
- package/lib/ops/index.js +7 -0
- package/lib/ops/versions.d.ts +33 -0
- package/lib/ops/versions.js +95 -0
- package/lib/readers/AtonParser.js +5 -7
- package/lib/readers/phillipsEroll.d.ts +3 -3
- package/lib/readers/phillipsEroll.js +4 -5
- package/lib/readers/spencerBar.d.ts +2 -2
- package/lib/readers/spencerBar.js +2 -3
- package/lib/readers/stanfordAton.d.ts +3 -3
- package/lib/readers/stanfordAton.js +2 -3
- package/lib/schema.json +0 -13
- package/lib/{ReproducingSystem.d.ts → systems/ReproducingSystem.d.ts} +3 -3
- package/lib/{TrackerBar.d.ts → systems/TrackerBar.d.ts} +48 -8
- package/lib/{TrackerBar.js → systems/TrackerBar.js} +11 -2
- package/lib/systems/index.d.ts +8 -2
- package/lib/systems/index.js +7 -1
- package/lib/systems/welte.d.ts +100 -0
- package/lib/systems/welte.js +119 -0
- package/lib/systems/welteLicensee/bar.d.ts +1 -1
- package/lib/systems/welteLicensee/bar.js +5 -2
- package/lib/systems/welteLicensee/system.d.ts +1 -1
- package/lib/systems/welteLicensee/system.js +1 -1
- package/lib/systems/welteT100/bar.d.ts +27 -1
- package/lib/systems/welteT100/bar.js +44 -2
- package/lib/systems/welteT100/system.d.ts +6 -39
- package/lib/systems/welteT100/system.js +19 -128
- package/lib/systems/welteT98/bar.d.ts +1 -1
- package/lib/systems/welteT98/bar.js +18 -2
- package/lib/systems/welteT98/system.d.ts +20 -38
- package/lib/systems/welteT98/system.js +20 -121
- package/lib/validate.d.ts +1 -1
- package/lib/{EditionView.d.ts → view/EditionView.d.ts} +17 -41
- package/lib/{EditionView.js → view/EditionView.js} +26 -75
- package/package.json +7 -2
- package/lib/Emulation.d.ts +0 -36
- package/lib/Emulation.js +0 -250
- package/lib/editionOps.d.ts +0 -260
- package/lib/editionOps.js +0 -1079
- package/lib/importJsonLd.js +0 -57
- /package/lib/{migrate.d.ts → io/migrate.d.ts} +0 -0
- /package/lib/{Agent.js → model/Agent.js} +0 -0
- /package/lib/{Assumption.js → model/Assumption.js} +0 -0
- /package/lib/{ConditionState.d.ts → model/ConditionState.d.ts} +0 -0
- /package/lib/{ConditionState.js → model/ConditionState.js} +0 -0
- /package/lib/{Edit.js → model/Edit.js} +0 -0
- /package/lib/{Edition.js → model/Edition.js} +0 -0
- /package/lib/{Feature.js → model/Feature.js} +0 -0
- /package/lib/{FeatureSource.js → model/FeatureSource.js} +0 -0
- /package/lib/{Perforator.js → model/Perforator.js} +0 -0
- /package/lib/{Quantity.d.ts → model/Quantity.d.ts} +0 -0
- /package/lib/{Quantity.js → model/Quantity.js} +0 -0
- /package/lib/{Symbol.js → model/Symbol.js} +0 -0
- /package/lib/{TrackCalibration.d.ts → model/TrackCalibration.d.ts} +0 -0
- /package/lib/{TrackCalibration.js → model/TrackCalibration.js} +0 -0
- /package/lib/{notes.d.ts → model/notes.d.ts} +0 -0
- /package/lib/{notes.js → model/notes.js} +0 -0
- /package/lib/{procedures.d.ts → model/procedures.d.ts} +0 -0
- /package/lib/{procedures.js → model/procedures.js} +0 -0
- /package/lib/{vocabulary.d.ts → model/vocabulary.d.ts} +0 -0
- /package/lib/{sorted.d.ts → shared/sorted.d.ts} +0 -0
- /package/lib/{sorted.js → shared/sorted.js} +0 -0
- /package/lib/{utils.d.ts → shared/utils.d.ts} +0 -0
- /package/lib/{utils.js → shared/utils.js} +0 -0
- /package/lib/{ReproducingSystem.js → systems/ReproducingSystem.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { keyOf } from "
|
|
2
|
-
import { distance, mm } from "
|
|
3
|
-
import { partitionPoint } from "
|
|
4
|
-
import { groupBy } from "
|
|
1
|
+
import { keyOf } from "../systems/TrackerBar.js";
|
|
2
|
+
import { distance, mm } from "../model/Quantity.js";
|
|
3
|
+
import { partitionPoint } from "../shared/sorted.js";
|
|
4
|
+
import { groupBy } from "../shared/utils.js";
|
|
5
5
|
export const defaultCollationTolerance = { toleranceStart: mm(5), toleranceEnd: mm(5) };
|
|
6
6
|
/** Where the window is centred at the start of a feature, on nothing where it names no offset. */
|
|
7
7
|
export const offsetStartOf = (tolerance) => tolerance.offsetStart ?? mm(0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { FeatureOrPatch } from "
|
|
2
|
-
import { RollCopy, Shift } from "
|
|
3
|
-
import { TrackerBar } from "
|
|
4
|
-
import { Millimeters } from "
|
|
1
|
+
import { FeatureOrPatch } from "../model/Feature.js";
|
|
2
|
+
import { RollCopy, Shift } from "../model/RollCopy.js";
|
|
3
|
+
import { TrackerBar } from "../systems/TrackerBar.js";
|
|
4
|
+
import { Millimeters } from "../model/Quantity.js";
|
|
5
5
|
export declare const applyShift: (shift: Shift, copy: RollCopy) => void;
|
|
6
6
|
/** Scales the copy's features away from the beginning of the roll, and records the factor. */
|
|
7
7
|
export declare const applyScale: (factor: number, copy: RollCopy) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { featuresOf } from "
|
|
2
|
-
import {
|
|
3
|
-
import { add, mm, scale, subtract } from "
|
|
1
|
+
import { featuresOf } from "../model/RollCopy.js";
|
|
2
|
+
import { defaultTrackerBar } from "../systems/index.js";
|
|
3
|
+
import { add, mm, scale, subtract } from "../model/Quantity.js";
|
|
4
4
|
/** Moves both ends of a span, the far end only where the span has one. */
|
|
5
5
|
const move = (span, by) => {
|
|
6
6
|
span.from = add(span.from, by);
|
|
@@ -15,43 +15,39 @@ const stretch = (span, factor) => {
|
|
|
15
15
|
};
|
|
16
16
|
const back = (shift) => ({ horizontal: scale(shift.horizontal, -1), vertical: scale(shift.vertical, -1) });
|
|
17
17
|
export const applyShift = (shift, copy) => {
|
|
18
|
-
if (copy.
|
|
18
|
+
if (copy.measurements.shift)
|
|
19
19
|
return;
|
|
20
20
|
featuresOf(copy).forEach(feature => {
|
|
21
21
|
move(feature.horizontal, shift.horizontal);
|
|
22
22
|
move(feature.vertical, shift.vertical);
|
|
23
23
|
});
|
|
24
|
-
copy.ops = [...copy.ops, 'shifted'];
|
|
25
24
|
copy.measurements.shift = shift;
|
|
26
25
|
};
|
|
27
26
|
/** Scales the copy's features away from the beginning of the roll, and records the factor. */
|
|
28
27
|
export const applyScale = (factor, copy) => {
|
|
29
|
-
if (copy.
|
|
28
|
+
if (copy.measurements.scale !== undefined)
|
|
30
29
|
return;
|
|
31
30
|
featuresOf(copy).forEach(feature => stretch(feature.horizontal, factor));
|
|
32
|
-
copy.ops = [...copy.ops, 'stretched'];
|
|
33
31
|
copy.measurements.scale = factor;
|
|
34
32
|
};
|
|
35
33
|
/** Takes the shift off the copy's features again, as far as one was applied. */
|
|
36
34
|
export const revertShift = (copy) => {
|
|
37
35
|
const shift = copy.measurements.shift;
|
|
38
|
-
if (!
|
|
36
|
+
if (!shift)
|
|
39
37
|
return;
|
|
40
38
|
const reversed = back(shift);
|
|
41
39
|
featuresOf(copy).forEach(feature => {
|
|
42
40
|
move(feature.horizontal, reversed.horizontal);
|
|
43
41
|
move(feature.vertical, reversed.vertical);
|
|
44
42
|
});
|
|
45
|
-
copy.ops = copy.ops.filter(op => op !== 'shifted');
|
|
46
43
|
delete copy.measurements.shift;
|
|
47
44
|
};
|
|
48
45
|
/** Takes the scale off the copy's features again, as far as one was applied. */
|
|
49
46
|
export const revertScale = (copy) => {
|
|
50
47
|
const factor = copy.measurements.scale;
|
|
51
|
-
if (
|
|
48
|
+
if (factor === undefined)
|
|
52
49
|
return;
|
|
53
50
|
featuresOf(copy).forEach(feature => stretch(feature.horizontal, 1 / factor));
|
|
54
|
-
copy.ops = copy.ops.filter(op => op !== 'stretched');
|
|
55
51
|
delete copy.measurements.scale;
|
|
56
52
|
};
|
|
57
53
|
const chainsOf = (copy) => featuresOf(copy).filter(feature => feature.type === 'HoleChain');
|
|
@@ -103,7 +99,7 @@ export const tooShortToShorten = (extension, copy, leaving = new Set()) => chain
|
|
|
103
99
|
* extension, `tooShortToShorten` saying beforehand which those are.
|
|
104
100
|
*/
|
|
105
101
|
export const shortenChains = (extension, copy, leaving = new Set()) => {
|
|
106
|
-
if (copy.
|
|
102
|
+
if (copy.measurements.readerExtension)
|
|
107
103
|
return;
|
|
108
104
|
const tooShort = tooShortToShorten(extension, copy, leaving);
|
|
109
105
|
if (tooShort.length > 0) {
|
|
@@ -115,19 +111,17 @@ export const shortenChains = (extension, copy, leaving = new Set()) => {
|
|
|
115
111
|
chainsOf(copy)
|
|
116
112
|
.filter(chain => !leaving.has(chain.id))
|
|
117
113
|
.forEach(chain => { chain.horizontal.to = subtract(chain.horizontal.to, extension); });
|
|
118
|
-
copy.ops = [...copy.ops, 'shortened'];
|
|
119
114
|
copy.measurements.readerExtension = { length: extension, ...(left.length > 0 && { leaving: left }) };
|
|
120
115
|
};
|
|
121
116
|
/** Puts the reader's extension back on the chains it was taken off, as far as one was taken off. */
|
|
122
117
|
export const revertShortening = (copy) => {
|
|
123
118
|
const taken = copy.measurements.readerExtension;
|
|
124
|
-
if (
|
|
119
|
+
if (taken === undefined)
|
|
125
120
|
return;
|
|
126
121
|
const left = new Set(taken.leaving ?? []);
|
|
127
122
|
chainsOf(copy)
|
|
128
123
|
.filter(chain => !left.has(chain.id))
|
|
129
124
|
.forEach(chain => { chain.horizontal.to = add(chain.horizontal.to, taken.length); });
|
|
130
|
-
copy.ops = copy.ops.filter(op => op !== 'shortened');
|
|
131
125
|
delete copy.measurements.readerExtension;
|
|
132
126
|
};
|
|
133
127
|
const byPlace = (x, y) => x.at - y.at || x.pitch - y.pitch;
|
|
@@ -251,7 +245,7 @@ const resultOf = ({ line, matches }) => {
|
|
|
251
245
|
* sounds, and it is the pitches that are matched. Where a copy's holes
|
|
252
246
|
* have already been put onto the edition's bar, that bar reads it.
|
|
253
247
|
*/
|
|
254
|
-
export function alignFeatures(rollA, rollB, barA =
|
|
248
|
+
export function alignFeatures(rollA, rollB, barA = defaultTrackerBar, barB = barA) {
|
|
255
249
|
const a = noteOnsets(rollA, barA);
|
|
256
250
|
const b = noteOnsets(rollB, barB);
|
|
257
251
|
const coarse = robustLine(anchors(a, b));
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Belief } from "
|
|
1
|
+
import { Belief } from "../model/Assumption.js";
|
|
2
2
|
import { BothEnds, CollationTolerance, Displacement } from "./Collation.js";
|
|
3
|
-
import { EditionView } from "
|
|
4
|
-
import { Millimeters } from "
|
|
3
|
+
import { EditionView } from "../view/EditionView.js";
|
|
4
|
+
import { Millimeters } from "../model/Quantity.js";
|
|
5
5
|
import { Histogram, Spread, Tail } from "./statistics.js";
|
|
6
|
-
import { AnySymbol } from "
|
|
6
|
+
import { AnySymbol } from "../model/Symbol.js";
|
|
7
7
|
/**
|
|
8
8
|
* How far the copies of a roll disagree about where a symbol lies, and
|
|
9
9
|
* the collation tolerance that follows from it.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { v4 } from "uuid";
|
|
2
2
|
import { admits, offsetEndOf, offsetStartOf } from "./Collation.js";
|
|
3
|
-
import { mean, mm } from "
|
|
3
|
+
import { mean, mm } from "../model/Quantity.js";
|
|
4
4
|
import { excessKurtosisOf, histogramOf, normalQuantile, spreadOf, standardise, tailOf } from "./statistics.js";
|
|
5
|
-
import { groupBy } from "
|
|
6
|
-
import { deletedBy, insertedBy } from "
|
|
5
|
+
import { groupBy } from "../shared/utils.js";
|
|
6
|
+
import { deletedBy, insertedBy } from "../model/Version.js";
|
|
7
7
|
const placeOf = (carriers) => ({
|
|
8
8
|
from: mean(carriers.map(carrier => carrier.horizontal.from)),
|
|
9
9
|
to: mean(carriers.map(carrier => carrier.horizontal.to))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { quantity } from "
|
|
1
|
+
import { quantity } from "../model/Quantity.js";
|
|
2
2
|
/**
|
|
3
3
|
* The statistics a sample of measurements is described with.
|
|
4
4
|
*
|
|
@@ -116,6 +116,6 @@ export const histogramOf = (values, binWidth) => {
|
|
|
116
116
|
counts: values.reduce((tally, value) => {
|
|
117
117
|
tally[Math.min(bins - 1, Math.floor((value - first) / binWidth))] += 1;
|
|
118
118
|
return tally;
|
|
119
|
-
},
|
|
119
|
+
}, Array.from({ length: bins }, () => 0))
|
|
120
120
|
};
|
|
121
121
|
};
|
|
@@ -1,29 +1,14 @@
|
|
|
1
1
|
import { CollationTolerance, Locate } from "./Collation.js";
|
|
2
|
-
import { AnySymbol, Expression } from "
|
|
3
|
-
import { Millimeters, Track } from "
|
|
4
|
-
import { TrackerBar } from "
|
|
2
|
+
import { AnySymbol, Expression } from "../model/Symbol.js";
|
|
3
|
+
import { Millimeters, Track } from "../model/Quantity.js";
|
|
4
|
+
import { Operation, TrackerBar } from "../systems/TrackerBar.js";
|
|
5
|
+
export type { Operation, Spelling } from "../systems/TrackerBar.js";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* What an expression type operates, as the bar that reads it says, or
|
|
8
|
+
* nothing for a word no bar here knows or one without a counterpart in
|
|
9
|
+
* another scale. The bars share their words where they share a scale,
|
|
10
|
+
* as the Licensee does the T-100's, so the first that reads it answers.
|
|
8
11
|
*/
|
|
9
|
-
export type Spelling = 'on' | 'off' | 'held';
|
|
10
|
-
/**
|
|
11
|
-
* What a word of a scale operates, rather than the word itself, so that
|
|
12
|
-
* two scales can be compared at all.
|
|
13
|
-
*/
|
|
14
|
-
export interface Operation {
|
|
15
|
-
/** The function operated, named the same wherever a scale has it. */
|
|
16
|
-
operates: string;
|
|
17
|
-
spelling: Spelling;
|
|
18
|
-
/**
|
|
19
|
-
* Whether the half of the keyboard the valve serves is part of the
|
|
20
|
-
* function operated. The dynamics are per half; the pedals are not,
|
|
21
|
-
* and the two Welte scales put them on opposite edges of the paper,
|
|
22
|
-
* so a rule that compared sides would leave every pedal unpaired.
|
|
23
|
-
*/
|
|
24
|
-
sided: boolean;
|
|
25
|
-
}
|
|
26
|
-
/** What an expression type operates, or nothing for a word no scale here knows. */
|
|
27
12
|
export declare const operationOf: (expressionType: string) => Operation | undefined;
|
|
28
13
|
/**
|
|
29
14
|
* A latched function of the older version and the held command of the
|
|
@@ -46,7 +31,7 @@ export interface Substitution {
|
|
|
46
31
|
* 2.66 mm grid with bridges of about a millimetre, so a scan read hole
|
|
47
32
|
* by hole shows a run where the paper shows one command.
|
|
48
33
|
*/
|
|
49
|
-
export declare const defaultChainGap: import("
|
|
34
|
+
export declare const defaultChainGap: import("../index.js").Quantity<"mm">;
|
|
50
35
|
/**
|
|
51
36
|
* The position a bar reads its own command for the same function on,
|
|
52
37
|
* where it has no word for this one.
|
|
@@ -1,44 +1,20 @@
|
|
|
1
1
|
import { defaultCollationTolerance } from "./Collation.js";
|
|
2
|
-
import { distance, mm, track } from "
|
|
2
|
+
import { distance, mm, track } from "../model/Quantity.js";
|
|
3
|
+
import { trackerBars } from "../systems/index.js";
|
|
3
4
|
/**
|
|
4
|
-
* What
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* holds it for as long as one perforation lasts (Hagmann pp. 89 f. and
|
|
9
|
-
* 100–103; Phillips p. 121), and Welte renamed two of the functions
|
|
10
|
-
* between the scales: the red `SlowCrescendo` is the green `Crescendo`,
|
|
11
|
-
* and the red's single `Forzando` valve answers to two green ones that
|
|
12
|
-
* name the end they pull towards. So the correspondence is stated here
|
|
13
|
-
* rather than derived, and only two types that operate one function can
|
|
14
|
-
* stand for each other.
|
|
15
|
-
*
|
|
16
|
-
* The T-100's `MotorOn`/`MotorOff`, `Rewind` and `ElectricCutOff` are
|
|
17
|
-
* left out on purpose: the green scale has no word for any of them, its
|
|
18
|
-
* motor switch being an automatic mercury contact (Skala-Rolle 98 §12)
|
|
19
|
-
* and its rewind riding on the bass sforzando-piano line. A red command
|
|
20
|
-
* of those kinds has no counterpart and stays a plain deletion.
|
|
5
|
+
* What an expression type operates, as the bar that reads it says, or
|
|
6
|
+
* nothing for a word no bar here knows or one without a counterpart in
|
|
7
|
+
* another scale. The bars share their words where they share a scale,
|
|
8
|
+
* as the Licensee does the T-100's, so the first that reads it answers.
|
|
21
9
|
*/
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
SustainPedalOn: { operates: 'sustainPedal', spelling: 'on', sided: false },
|
|
30
|
-
SustainPedalOff: { operates: 'sustainPedal', spelling: 'off', sided: false },
|
|
31
|
-
SoftPedalOn: { operates: 'softPedal', spelling: 'on', sided: false },
|
|
32
|
-
SoftPedalOff: { operates: 'softPedal', spelling: 'off', sided: false },
|
|
33
|
-
Mezzoforte: { operates: 'mezzoforte', spelling: 'held', sided: true },
|
|
34
|
-
Crescendo: { operates: 'crescendo', spelling: 'held', sided: true },
|
|
35
|
-
SforzandoForte: { operates: 'sforzando', spelling: 'held', sided: true },
|
|
36
|
-
SforzandoPiano: { operates: 'sforzando', spelling: 'held', sided: true },
|
|
37
|
-
SustainPedal: { operates: 'sustainPedal', spelling: 'held', sided: false },
|
|
38
|
-
SoftPedal: { operates: 'softPedal', spelling: 'held', sided: false }
|
|
10
|
+
export const operationOf = (expressionType) => {
|
|
11
|
+
for (const bar of trackerBars) {
|
|
12
|
+
const operation = bar.operationOf(expressionType);
|
|
13
|
+
if (operation)
|
|
14
|
+
return operation;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
39
17
|
};
|
|
40
|
-
/** What an expression type operates, or nothing for a word no scale here knows. */
|
|
41
|
-
export const operationOf = (expressionType) => operations[expressionType];
|
|
42
18
|
/**
|
|
43
19
|
* How wide a paper bridge may be for two held perforations to be one
|
|
44
20
|
* command. The T-98 punches a hold as a chain of round holes on a
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { MidiFile } from "midifile-ts";
|
|
2
|
+
import { EditionView } from "../view/EditionView.js";
|
|
3
|
+
import type { TrackerBar } from "../systems/TrackerBar.js";
|
|
4
|
+
import { Version } from "../model/Version.js";
|
|
5
|
+
import { AnyPerformedRollFeature, EmulatedCurve, NegotiatedEvent, ReproducingSystem } from "../systems/ReproducingSystem.js";
|
|
6
|
+
import { Millimeters } from "../model/Quantity.js";
|
|
7
|
+
export type EmulationScope = {
|
|
8
|
+
/** Only notes whose onset lies within this span of the roll are played. */
|
|
9
|
+
range?: [Millimeters, Millimeters];
|
|
10
|
+
/** Start the clock at the first note rather than at the beginning of the roll. */
|
|
11
|
+
skipToFirstNote?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The events moved to where their statements put them, the events given
|
|
15
|
+
* left as they were. The view supplies the copies, whose measurements
|
|
16
|
+
* decide how far before or after its reference a command goes, and a
|
|
17
|
+
* punch diameter, or a millimetre, where no copy agrees with a statement.
|
|
18
|
+
*/
|
|
19
|
+
export declare const withPlacementsApplied: (view: EditionView, events: readonly NegotiatedEvent[]) => NegotiatedEvent[];
|
|
20
|
+
/**
|
|
21
|
+
* The commands of a version as the bar performs them, in order of place,
|
|
22
|
+
* within the scope and with the placements applied. A note plays only
|
|
23
|
+
* where its onset falls in the range; expressions play throughout.
|
|
24
|
+
*/
|
|
25
|
+
export declare const negotiatedEventsOf: (view: EditionView, version: Readonly<Version>, bar: TrackerBar, { range }?: Pick<EmulationScope, 'range'>) => NegotiatedEvent[];
|
|
26
|
+
/** A version performed: what was negotiated, and what the system made of it. */
|
|
27
|
+
export interface Emulated {
|
|
28
|
+
/** The id of the version performed. */
|
|
29
|
+
readonly source: string;
|
|
30
|
+
readonly negotiated: readonly NegotiatedEvent[];
|
|
31
|
+
/** The performed events in order of time, from the origin the scope sets. */
|
|
32
|
+
readonly events: readonly AnyPerformedRollFeature[];
|
|
33
|
+
readonly curves: readonly EmulatedCurve[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Performs a version on a reproducing system: the symbols are negotiated
|
|
37
|
+
* into placed events and the system plays them. Nothing given is changed.
|
|
38
|
+
*/
|
|
39
|
+
export declare const emulate: <Options extends object>(system: ReproducingSystem<Options>, version: Readonly<Version>, view: EditionView, options?: Options, { range, skipToFirstNote }?: EmulationScope) => Emulated;
|
|
40
|
+
/**
|
|
41
|
+
* The performed events as a MIDI file, in which every note and pedal step
|
|
42
|
+
* is labelled with the symbol it performs. The file names the system, the
|
|
43
|
+
* version and the options it was played with.
|
|
44
|
+
*/
|
|
45
|
+
export declare const midiOf: (events: readonly AnyPerformedRollFeature[], systemName: string, options: object, source?: string) => MidiFile;
|
|
46
|
+
/**
|
|
47
|
+
* A version of the edition, performed and kept: `emulate` and `midiOf`
|
|
48
|
+
* with the result held on the object between the two calls. Prefer the
|
|
49
|
+
* functions where nothing needs holding.
|
|
50
|
+
*/
|
|
51
|
+
export declare class Emulation<Options extends object> {
|
|
52
|
+
readonly system: ReproducingSystem<Options>;
|
|
53
|
+
options: Options;
|
|
54
|
+
midiEvents: AnyPerformedRollFeature[];
|
|
55
|
+
negotiatedEvents: NegotiatedEvent[];
|
|
56
|
+
curves: readonly EmulatedCurve[];
|
|
57
|
+
source?: string;
|
|
58
|
+
constructor(system: ReproducingSystem<Options>, options?: Options);
|
|
59
|
+
/** Moves the negotiated events to where their statements put them; see `withPlacementsApplied`. */
|
|
60
|
+
applyConstraints(view: EditionView): void;
|
|
61
|
+
emulateVersion(version: Version, view: EditionView, scope?: EmulationScope): AnyPerformedRollFeature[];
|
|
62
|
+
findEventsPerforming(id: string): AnyPerformedRollFeature[];
|
|
63
|
+
asMIDI(): MidiFile;
|
|
64
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { MIDIControlEvents } from "midifile-ts";
|
|
2
|
+
import { idOf } from "../model/Assumption.js";
|
|
3
|
+
import { isCommand, pairsAmong, placementsOf } from "../model/Symbol.js";
|
|
4
|
+
import { add, mean, mm, seconds, subtract } from "../model/Quantity.js";
|
|
5
|
+
import { punchDiameterOf } from "../model/RollCopy.js";
|
|
6
|
+
import { toOwnPaperOf } from "../analysis/ownPaper.js";
|
|
7
|
+
import { negotiatedEventOf } from "./negotiation.js";
|
|
8
|
+
/** The punch diameter the edition's copies report, where any of them does. */
|
|
9
|
+
const meanPunchDiameterOf = (view) => {
|
|
10
|
+
const measured = view.edition.copies
|
|
11
|
+
.map(punchDiameterOf)
|
|
12
|
+
.filter(value => value !== undefined);
|
|
13
|
+
return measured.length > 0 ? mean(measured) : undefined;
|
|
14
|
+
};
|
|
15
|
+
const propertiesOf = (view, version) => ({
|
|
16
|
+
punchDiameter: meanPunchDiameterOf(view),
|
|
17
|
+
tempo: view.edition.tempoAdjustment,
|
|
18
|
+
toOwnPaper: toOwnPaperOf(view, version)
|
|
19
|
+
});
|
|
20
|
+
/** The onset a symbol has on each copy carrying it, by the copy's id, as the mean of its chains there. */
|
|
21
|
+
const onsetsByCopy = (view, symbolId) => {
|
|
22
|
+
const symbol = view.symbol(symbolId);
|
|
23
|
+
if (!symbol)
|
|
24
|
+
return new Map();
|
|
25
|
+
const onsets = view.placedCarriersOf(symbol).flatMap((carrier) => {
|
|
26
|
+
const copy = view.copyOf(carrier.id);
|
|
27
|
+
return copy ? [[copy.id, carrier.horizontal.from]] : [];
|
|
28
|
+
});
|
|
29
|
+
const copies = new Set(onsets.map(([copy]) => copy));
|
|
30
|
+
return new Map([...copies].map(copy => [copy, mean(onsets.filter(([at]) => at === copy).map(([, onset]) => onset))]));
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The measured distance from the onset of the reference to the onset of
|
|
34
|
+
* the follower on each copy carrying both, negative where the follower
|
|
35
|
+
* comes first there.
|
|
36
|
+
*/
|
|
37
|
+
const offsetsBetween = (view, followerId, referenceId) => {
|
|
38
|
+
const references = onsetsByCopy(view, referenceId);
|
|
39
|
+
return [...onsetsByCopy(view, followerId)].flatMap(([copy, onset]) => {
|
|
40
|
+
const reference = references.get(copy);
|
|
41
|
+
return reference === undefined ? [] : [subtract(onset, reference)];
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* How far before or after its reference a follower is put when the
|
|
46
|
+
* measurement does not already have it there: as far as the copies
|
|
47
|
+
* that agree with the statement put it, and one gap where none does.
|
|
48
|
+
*/
|
|
49
|
+
const distanceOnSide = (side, offsets, gap) => {
|
|
50
|
+
const agreeing = offsets.filter(offset => side === 'before' ? offset < 0 : offset > 0);
|
|
51
|
+
return agreeing.length > 0 ? mm(Math.abs(mean(agreeing))) : gap;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* How far each event has to move for the placements to hold:
|
|
55
|
+
* an aligned event takes the onset of its reference, one placed before
|
|
56
|
+
* or after its reference keeps its place where the measurement has it
|
|
57
|
+
* on that side and is moved there where it does not, and a paired
|
|
58
|
+
* event follows its partner so that the distance between the two is
|
|
59
|
+
* kept. Events that stay where they are do not appear.
|
|
60
|
+
*/
|
|
61
|
+
const displacementsOf = (events, offsetsBetween, gap) => {
|
|
62
|
+
const byId = new Map(events.map(event => [event.id, event]));
|
|
63
|
+
/** Where an event comes to lie once its statement and those of its references hold. */
|
|
64
|
+
const placedOnsetOf = (event, visited = new Set()) => {
|
|
65
|
+
const measured = event.horizontal.from;
|
|
66
|
+
const placement = placementsOf(event)[0];
|
|
67
|
+
const reference = placement && byId.get(idOf(placement.reference));
|
|
68
|
+
if (!placement || !reference || reference.id === event.id || visited.has(event.id))
|
|
69
|
+
return measured;
|
|
70
|
+
const referenceOnset = placedOnsetOf(reference, new Set([...visited, event.id]));
|
|
71
|
+
const distance = (side) => distanceOnSide(side, offsetsBetween(event.id, reference.id), gap);
|
|
72
|
+
switch (placement.relation) {
|
|
73
|
+
case 'alignedWith': return referenceOnset;
|
|
74
|
+
case 'before': return measured < referenceOnset ? measured : subtract(referenceOnset, distance('before'));
|
|
75
|
+
case 'after': return measured > referenceOnset ? measured : add(referenceOnset, distance('after'));
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const displacements = new Map(events
|
|
79
|
+
.map((event) => [event, subtract(placedOnsetOf(event), event.horizontal.from)])
|
|
80
|
+
.filter(([, distance]) => distance !== 0));
|
|
81
|
+
pairsAmong(events).forEach(([one, other]) => {
|
|
82
|
+
const shared = displacements.get(one) ?? displacements.get(other);
|
|
83
|
+
if (shared === undefined)
|
|
84
|
+
return;
|
|
85
|
+
displacements.set(one, shared);
|
|
86
|
+
displacements.set(other, shared);
|
|
87
|
+
});
|
|
88
|
+
return displacements;
|
|
89
|
+
};
|
|
90
|
+
/** The earliest of the times, or the beginning of the roll where there are none. */
|
|
91
|
+
const earliestOf = (times) => times.length === 0 ? seconds(0) : times.reduce((soonest, at) => at < soonest ? at : soonest);
|
|
92
|
+
/**
|
|
93
|
+
* The events moved to where their statements put them, the events given
|
|
94
|
+
* left as they were. The view supplies the copies, whose measurements
|
|
95
|
+
* decide how far before or after its reference a command goes, and a
|
|
96
|
+
* punch diameter, or a millimetre, where no copy agrees with a statement.
|
|
97
|
+
*/
|
|
98
|
+
export const withPlacementsApplied = (view, events) => {
|
|
99
|
+
const gap = meanPunchDiameterOf(view) ?? mm(1);
|
|
100
|
+
const offsets = (follower, reference) => offsetsBetween(view, follower, reference);
|
|
101
|
+
const displacements = displacementsOf(events, offsets, gap);
|
|
102
|
+
return events.map(event => {
|
|
103
|
+
const distance = displacements.get(event);
|
|
104
|
+
return distance === undefined ? event : {
|
|
105
|
+
...event,
|
|
106
|
+
horizontal: { ...event.horizontal, from: add(event.horizontal.from, distance), to: add(event.horizontal.to, distance) }
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The commands of a version as the bar performs them, in order of place,
|
|
112
|
+
* within the scope and with the placements applied. A note plays only
|
|
113
|
+
* where its onset falls in the range; expressions play throughout.
|
|
114
|
+
*/
|
|
115
|
+
export const negotiatedEventsOf = (view, version, bar, { range } = {}) => {
|
|
116
|
+
const inScope = (event) => !range || event.type !== 'note' || (event.horizontal.from > range[0] && event.horizontal.from < range[1]);
|
|
117
|
+
return withPlacementsApplied(view, view.snapshot(version.id)
|
|
118
|
+
.filter(isCommand)
|
|
119
|
+
.map(symbol => negotiatedEventOf(view, symbol, bar))
|
|
120
|
+
.filter(event => event !== null)
|
|
121
|
+
.filter(inScope));
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Performs a version on a reproducing system: the symbols are negotiated
|
|
125
|
+
* into placed events and the system plays them. Nothing given is changed.
|
|
126
|
+
*/
|
|
127
|
+
export const emulate = (system, version, view, options = system.defaultOptions, { range, skipToFirstNote = false } = {}) => {
|
|
128
|
+
const negotiated = negotiatedEventsOf(view, version, system.trackerBar, { range });
|
|
129
|
+
if (negotiated.length === 0)
|
|
130
|
+
return { source: version.id, negotiated, events: [], curves: [] };
|
|
131
|
+
const performance = system.perform(negotiated, options, propertiesOf(view, version));
|
|
132
|
+
const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
|
|
133
|
+
const origin = skipToFirstNote ? earliestOf(onsets) : seconds(0);
|
|
134
|
+
return {
|
|
135
|
+
source: version.id,
|
|
136
|
+
negotiated,
|
|
137
|
+
curves: performance.curves,
|
|
138
|
+
events: performance.events
|
|
139
|
+
.map(event => ({ ...event, at: subtract(event.at, origin) }))
|
|
140
|
+
.filter(event => event.at >= 0)
|
|
141
|
+
.sort((a, b) => a.at - b.at)
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* The performed events as a MIDI file, in which every note and pedal step
|
|
146
|
+
* is labelled with the symbol it performs. The file names the system, the
|
|
147
|
+
* version and the options it was played with.
|
|
148
|
+
*/
|
|
149
|
+
export const midiOf = (events, systemName, options, source) => {
|
|
150
|
+
const TICKS_PER_SECOND = 1000;
|
|
151
|
+
const midi = [];
|
|
152
|
+
const text = (text, deltaTime = 0) => ({ type: 'meta', subtype: 'text', text, deltaTime });
|
|
153
|
+
const controller = (controllerType, value, deltaTime = 0) => ({ type: 'channel', subtype: 'controller', controllerType, value, deltaTime, channel: 0 });
|
|
154
|
+
const controllerOf = (event) => event.type === 'damper' ? MIDIControlEvents.SUSTAIN : MIDIControlEvents.SOFT_PEDAL;
|
|
155
|
+
midi.push(text(`linked-rolls (${systemName})`));
|
|
156
|
+
if (source) {
|
|
157
|
+
midi.push(text(source));
|
|
158
|
+
}
|
|
159
|
+
for (const [key, value] of Object.entries(options)) {
|
|
160
|
+
midi.push(text(`${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`));
|
|
161
|
+
}
|
|
162
|
+
midi.push({
|
|
163
|
+
type: 'meta',
|
|
164
|
+
subtype: 'setTempo',
|
|
165
|
+
microsecondsPerBeat: 1000000,
|
|
166
|
+
deltaTime: 0
|
|
167
|
+
});
|
|
168
|
+
// both pedals start at rest
|
|
169
|
+
midi.push(controller(MIDIControlEvents.SUSTAIN, 0), controller(MIDIControlEvents.SOFT_PEDAL, 0));
|
|
170
|
+
// a pedal step is labelled with its command only where that
|
|
171
|
+
// command changes, so the file is not swamped with labels
|
|
172
|
+
const lastCause = {};
|
|
173
|
+
let currentTick = 0;
|
|
174
|
+
for (const event of events.toSorted((a, b) => a.at - b.at)) {
|
|
175
|
+
const tick = Math.round(event.at * TICKS_PER_SECOND);
|
|
176
|
+
const deltaTime = tick - currentTick;
|
|
177
|
+
currentTick = tick;
|
|
178
|
+
if (event.type === 'noteOn') {
|
|
179
|
+
midi.push(text(event.performs.id, deltaTime));
|
|
180
|
+
midi.push({
|
|
181
|
+
type: 'channel',
|
|
182
|
+
subtype: 'noteOn',
|
|
183
|
+
noteNumber: event.pitch,
|
|
184
|
+
velocity: +event.velocity.toFixed(0),
|
|
185
|
+
deltaTime: 0,
|
|
186
|
+
channel: 0
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else if (event.type === 'noteOff') {
|
|
190
|
+
midi.push({
|
|
191
|
+
type: 'channel',
|
|
192
|
+
subtype: 'noteOff',
|
|
193
|
+
noteNumber: event.pitch,
|
|
194
|
+
velocity: 127,
|
|
195
|
+
deltaTime,
|
|
196
|
+
channel: 0
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const labelled = lastCause[event.type] === event.performs.id;
|
|
201
|
+
lastCause[event.type] = event.performs.id;
|
|
202
|
+
if (!labelled) {
|
|
203
|
+
midi.push(text(event.performs.id, deltaTime));
|
|
204
|
+
}
|
|
205
|
+
midi.push(controller(controllerOf(event), event.value, labelled ? deltaTime : 0));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
header: {
|
|
210
|
+
ticksPerBeat: TICKS_PER_SECOND,
|
|
211
|
+
formatType: 0,
|
|
212
|
+
trackCount: 1
|
|
213
|
+
},
|
|
214
|
+
tracks: [midi]
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* A version of the edition, performed and kept: `emulate` and `midiOf`
|
|
219
|
+
* with the result held on the object between the two calls. Prefer the
|
|
220
|
+
* functions where nothing needs holding.
|
|
221
|
+
*/
|
|
222
|
+
export class Emulation {
|
|
223
|
+
constructor(system, options = system.defaultOptions) {
|
|
224
|
+
Object.defineProperty(this, "system", {
|
|
225
|
+
enumerable: true,
|
|
226
|
+
configurable: true,
|
|
227
|
+
writable: true,
|
|
228
|
+
value: void 0
|
|
229
|
+
});
|
|
230
|
+
Object.defineProperty(this, "options", {
|
|
231
|
+
enumerable: true,
|
|
232
|
+
configurable: true,
|
|
233
|
+
writable: true,
|
|
234
|
+
value: void 0
|
|
235
|
+
});
|
|
236
|
+
Object.defineProperty(this, "midiEvents", {
|
|
237
|
+
enumerable: true,
|
|
238
|
+
configurable: true,
|
|
239
|
+
writable: true,
|
|
240
|
+
value: []
|
|
241
|
+
});
|
|
242
|
+
// sorted list of events with the negotiated assumptions already applied
|
|
243
|
+
Object.defineProperty(this, "negotiatedEvents", {
|
|
244
|
+
enumerable: true,
|
|
245
|
+
configurable: true,
|
|
246
|
+
writable: true,
|
|
247
|
+
value: []
|
|
248
|
+
});
|
|
249
|
+
Object.defineProperty(this, "curves", {
|
|
250
|
+
enumerable: true,
|
|
251
|
+
configurable: true,
|
|
252
|
+
writable: true,
|
|
253
|
+
value: []
|
|
254
|
+
});
|
|
255
|
+
Object.defineProperty(this, "source", {
|
|
256
|
+
enumerable: true,
|
|
257
|
+
configurable: true,
|
|
258
|
+
writable: true,
|
|
259
|
+
value: void 0
|
|
260
|
+
});
|
|
261
|
+
this.system = system;
|
|
262
|
+
this.options = options;
|
|
263
|
+
}
|
|
264
|
+
/** Moves the negotiated events to where their statements put them; see `withPlacementsApplied`. */
|
|
265
|
+
applyConstraints(view) {
|
|
266
|
+
this.negotiatedEvents = withPlacementsApplied(view, this.negotiatedEvents);
|
|
267
|
+
}
|
|
268
|
+
emulateVersion(version, view, scope = {}) {
|
|
269
|
+
const emulated = emulate(this.system, version, view, this.options, scope);
|
|
270
|
+
this.source = emulated.source;
|
|
271
|
+
this.negotiatedEvents = [...emulated.negotiated];
|
|
272
|
+
this.curves = emulated.curves;
|
|
273
|
+
this.midiEvents = [...emulated.events];
|
|
274
|
+
return this.midiEvents;
|
|
275
|
+
}
|
|
276
|
+
findEventsPerforming(id) {
|
|
277
|
+
return this.midiEvents.filter(event => event.performs.id === id);
|
|
278
|
+
}
|
|
279
|
+
asMIDI() {
|
|
280
|
+
return midiOf(this.midiEvents, this.system.name, this.options, this.source);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EditionView } from "../view/EditionView.js";
|
|
2
|
+
import type { Expression, Note } from "../model/Symbol.js";
|
|
3
|
+
import type { TrackerBar } from "../systems/TrackerBar.js";
|
|
4
|
+
import type { NegotiatedEvent } from "../systems/ReproducingSystem.js";
|
|
5
|
+
/**
|
|
6
|
+
* The symbol as a performance needs it: where it lies, and the
|
|
7
|
+
* position the performing bar reads it on.
|
|
8
|
+
*
|
|
9
|
+
* Nothing where that bar reads nothing of it, which is the case a
|
|
10
|
+
* transfer between systems leaves behind: a red `ForzandoOn` a
|
|
11
|
+
* green version still inherits cannot be performed on a green
|
|
12
|
+
* machine, and an edit has yet to say what took its place.
|
|
13
|
+
*/
|
|
14
|
+
export declare const negotiatedEventOf: (view: EditionView, symbol: Note | Expression, bar: TrackerBar) => NegotiatedEvent | null;
|