linked-rolls 0.10.1 → 0.12.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/Collation.d.ts +1 -1
- package/lib/Collation.js +53 -14
- package/lib/EditionView.d.ts +11 -0
- package/lib/EditionView.js +78 -82
- package/lib/Emulation.js +10 -16
- package/lib/Quantity.d.ts +4 -0
- package/lib/Quantity.js +3 -0
- package/lib/RollCopy.d.ts +43 -1
- package/lib/TrackerBar.d.ts +16 -1
- package/lib/TrackerBar.js +21 -0
- package/lib/Version.d.ts +5 -0
- package/lib/Version.js +4 -0
- package/lib/alignment.d.ts +8 -8
- package/lib/alignment.js +15 -15
- package/lib/constraints.js +4 -2
- package/lib/editionOps.d.ts +13 -5
- package/lib/editionOps.js +87 -37
- package/lib/importJsonLd.d.ts +3 -1
- package/lib/importJsonLd.js +22 -42
- package/lib/index.d.ts +3 -1
- package/lib/index.js +3 -1
- package/lib/migrate.js +34 -6
- package/lib/readers/spencerBar.d.ts +34 -0
- package/lib/readers/spencerBar.js +141 -0
- package/lib/readers/stanfordAton.d.ts +9 -1
- package/lib/readers/stanfordAton.js +30 -24
- package/lib/schema.json +90 -1
- package/lib/sorted.d.ts +6 -0
- package/lib/sorted.js +17 -0
- package/lib/spec/context.json +11 -1
- package/lib/systems/index.d.ts +6 -0
- package/lib/systems/index.js +10 -0
- package/lib/systems/welteLicensee/bar.d.ts +11 -0
- package/lib/systems/welteLicensee/bar.js +38 -0
- package/lib/systems/welteT100/bar.d.ts +2 -1
- package/lib/systems/welteT100/bar.js +4 -2
- package/lib/systems/welteT100/system.js +5 -4
- package/package.json +1 -1
- package/lib/readers/midiSpans.d.ts +0 -25
- package/lib/readers/midiSpans.js +0 -112
- package/lib/readers/spencerMidi.d.ts +0 -27
- package/lib/readers/spencerMidi.js +0 -60
package/lib/Collation.d.ts
CHANGED
|
@@ -26,5 +26,5 @@ export type Collation = {
|
|
|
26
26
|
symbol: Readonly<AnySymbol>;
|
|
27
27
|
counterpart: Readonly<AnySymbol>;
|
|
28
28
|
};
|
|
29
|
-
/** Each of the own symbols with every inherited symbol it collates with. */
|
|
29
|
+
/** Each of the own symbols with every inherited symbol it collates with, both in the order given. */
|
|
30
30
|
export declare const collationsOf: (own: readonly Readonly<AnySymbol>[], inherited: readonly Readonly<AnySymbol>[], locate: Locate, tolerance?: CollationTolerance) => Collation[];
|
package/lib/Collation.js
CHANGED
|
@@ -1,26 +1,65 @@
|
|
|
1
1
|
import { distance, mm } from "./Quantity";
|
|
2
|
+
import { partitionPoint } from "./sorted";
|
|
2
3
|
export const defaultCollationTolerance = { toleranceStart: mm(5), toleranceEnd: mm(5) };
|
|
4
|
+
/**
|
|
5
|
+
* What a symbol says, as a key: the pitch of a note, the type and scope
|
|
6
|
+
* of an expression. Symbols collate within one key only.
|
|
7
|
+
*/
|
|
8
|
+
const kindOf = (symbol) => {
|
|
9
|
+
switch (symbol.type) {
|
|
10
|
+
case 'note': return `note ${symbol.pitch}`;
|
|
11
|
+
case 'expression': return `expression ${symbol.scope} ${symbol.expressionType}`;
|
|
12
|
+
case 'text': return 'text';
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const nearby = (here, there, tolerance) => distance(here.from, there.from) <= tolerance.toleranceStart
|
|
16
|
+
&& distance(here.to, there.to) <= tolerance.toleranceEnd;
|
|
3
17
|
/**
|
|
4
18
|
* Two symbols collate when they are of one kind, say the same thing
|
|
5
19
|
* (pitch, or expression type and scope), and lie at about the same
|
|
6
20
|
* place along the roll.
|
|
7
21
|
*/
|
|
8
22
|
export const isCollatable = (a, b, locate, tolerance = defaultCollationTolerance) => {
|
|
9
|
-
if (a
|
|
10
|
-
return false;
|
|
11
|
-
if (a.type === 'note' && b.type === 'note' && a.pitch !== b.pitch)
|
|
12
|
-
return false;
|
|
13
|
-
if (a.type === 'expression' && b.type === 'expression'
|
|
14
|
-
&& (a.expressionType !== b.expressionType || a.scope !== b.scope))
|
|
23
|
+
if (kindOf(a) !== kindOf(b))
|
|
15
24
|
return false;
|
|
16
25
|
const here = locate(a)?.horizontal;
|
|
17
26
|
const there = locate(b)?.horizontal;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
return here !== undefined && there !== undefined && nearby(here, there, tolerance);
|
|
28
|
+
};
|
|
29
|
+
const placed = (symbols, locate) => symbols.flatMap((symbol, index) => {
|
|
30
|
+
const horizontal = locate(symbol)?.horizontal;
|
|
31
|
+
return horizontal ? [{ symbol, index, horizontal }] : [];
|
|
32
|
+
});
|
|
33
|
+
const groupBy = (items, keyOf) => items.reduce((groups, item) => {
|
|
34
|
+
const key = keyOf(item);
|
|
35
|
+
const group = groups.get(key);
|
|
36
|
+
if (group)
|
|
37
|
+
group.push(item);
|
|
38
|
+
else
|
|
39
|
+
groups.set(key, [item]);
|
|
40
|
+
return groups;
|
|
41
|
+
}, new Map());
|
|
42
|
+
/** The placed symbols by kind, each kind in order of onset. */
|
|
43
|
+
const byKindInOrderOfOnset = (symbols) => {
|
|
44
|
+
const groups = groupBy(symbols, ({ symbol }) => kindOf(symbol));
|
|
45
|
+
groups.forEach(group => group.sort((a, b) => a.horizontal.from - b.horizontal.from));
|
|
46
|
+
return groups;
|
|
47
|
+
};
|
|
48
|
+
/** Widens the onset window by a hair, so that rounding in its bounds cannot leave out what `nearby` accepts. */
|
|
49
|
+
const WINDOW_SLACK = 1e-9;
|
|
50
|
+
/** The symbols of a kind whose onset lies within the start tolerance of the span. */
|
|
51
|
+
const nearOnsetOf = (kind, span, tolerance) => {
|
|
52
|
+
const lowest = span.from - tolerance.toleranceStart - WINDOW_SLACK;
|
|
53
|
+
const highest = span.from + tolerance.toleranceStart + WINDOW_SLACK;
|
|
54
|
+
const first = partitionPoint(kind, candidate => candidate.horizontal.from < lowest);
|
|
55
|
+
const end = partitionPoint(kind, candidate => candidate.horizontal.from <= highest);
|
|
56
|
+
return kind.slice(first, end);
|
|
57
|
+
};
|
|
58
|
+
/** Each of the own symbols with every inherited symbol it collates with, both in the order given. */
|
|
59
|
+
export const collationsOf = (own, inherited, locate, tolerance = defaultCollationTolerance) => {
|
|
60
|
+
const kinds = byKindInOrderOfOnset(placed(inherited, locate));
|
|
61
|
+
return placed(own, locate).flatMap(({ symbol, horizontal }) => nearOnsetOf(kinds.get(kindOf(symbol)) ?? [], horizontal, tolerance)
|
|
62
|
+
.filter(candidate => nearby(horizontal, candidate.horizontal, tolerance))
|
|
63
|
+
.sort((a, b) => a.index - b.index)
|
|
64
|
+
.map(({ symbol: counterpart }) => ({ symbol, counterpart })));
|
|
22
65
|
};
|
|
23
|
-
/** Each of the own symbols with every inherited symbol it collates with. */
|
|
24
|
-
export const collationsOf = (own, inherited, locate, tolerance = defaultCollationTolerance) => own.flatMap(symbol => inherited
|
|
25
|
-
.filter(candidate => isCollatable(symbol, candidate, locate, tolerance))
|
|
26
|
-
.map(counterpart => ({ symbol, counterpart })));
|
package/lib/EditionView.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { HorizontalSpan, VerticalSpan, AnyFeature } from "./Feature";
|
|
|
3
3
|
import { AnySymbol, Expression, Note } from "./Symbol";
|
|
4
4
|
import { Version } from "./Version";
|
|
5
5
|
import { NegotiatedEvent } from "./ReproducingSystem";
|
|
6
|
+
import { Millimeters } from "./Quantity";
|
|
6
7
|
export type Path = (string | number)[];
|
|
7
8
|
export declare const getAt: <T>(path: Path, obj: unknown) => T | undefined;
|
|
8
9
|
export declare class EditionView {
|
|
@@ -27,12 +28,22 @@ export declare class EditionView {
|
|
|
27
28
|
getPath(anyId: string): Path | undefined;
|
|
28
29
|
linksTo(anyId: string): Path[];
|
|
29
30
|
travelUp(versionId: string, callback: (version: Readonly<Version>) => void): void;
|
|
31
|
+
/** The version and its ancestors, from the version up to the root. */
|
|
32
|
+
private lineageOf;
|
|
30
33
|
carriersOf(symbol: AnySymbol): Readonly<AnyFeature>[];
|
|
31
34
|
predecessorOf(versionId: string): Readonly<Version> | undefined;
|
|
32
35
|
dimensionOf(symbol: AnySymbol): Readonly<{
|
|
33
36
|
horizontal: HorizontalSpan;
|
|
34
37
|
vertical: VerticalSpan;
|
|
35
38
|
}> | undefined;
|
|
39
|
+
/** Where the symbol begins, as the mean onset of its carriers, or nothing for a symbol without a place. */
|
|
40
|
+
onsetOf(symbol: AnySymbol): Millimeters | undefined;
|
|
41
|
+
/** The symbols by onset, those without a place first; symbols at one place keep their order. */
|
|
42
|
+
private inOrderOfPlace;
|
|
43
|
+
/**
|
|
44
|
+
* The symbols the version shows: what it and its ancestors insert,
|
|
45
|
+
* each version's deletions striking what it or its ancestors inserted.
|
|
46
|
+
*/
|
|
36
47
|
snapshot(versionId: string): readonly Readonly<AnySymbol>[];
|
|
37
48
|
/**
|
|
38
49
|
* Assigns a generation (depth) to every node.
|
package/lib/EditionView.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deletedBy, insertedBy } from "./Version";
|
|
1
2
|
import { idOf, idsOf } from "./Assumption";
|
|
2
3
|
import { mean } from "./Quantity";
|
|
3
4
|
export const getAt = (path, obj) => {
|
|
@@ -9,11 +10,17 @@ export const getAt = (path, obj) => {
|
|
|
9
10
|
}
|
|
10
11
|
return node;
|
|
11
12
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/** Keys under which an object names others by id, singly or in a list. */
|
|
14
|
+
const referenceKeys = ['delete', 'comprehends', 'motivation'];
|
|
15
|
+
const isObject = (v) => v !== null && typeof v === "object";
|
|
16
|
+
/** An object that names another by its id and says nothing else, save perhaps a belief about the reference. */
|
|
17
|
+
const isReferenceOnly = (keys) => keys.every(key => key === 'id' || key === '@annotation');
|
|
18
|
+
const pathOf = (trail) => {
|
|
19
|
+
const path = [];
|
|
20
|
+
for (let link = trail; link !== null; link = link.up)
|
|
21
|
+
path.push(link.key);
|
|
22
|
+
return path.reverse();
|
|
23
|
+
};
|
|
17
24
|
export class EditionView {
|
|
18
25
|
constructor(edition) {
|
|
19
26
|
Object.defineProperty(this, "edition", {
|
|
@@ -58,63 +65,48 @@ export class EditionView {
|
|
|
58
65
|
}
|
|
59
66
|
indexObjects() {
|
|
60
67
|
const visited = new WeakSet();
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
if (
|
|
68
|
+
const link = (id, trail) => {
|
|
69
|
+
const trails = this.links.get(id);
|
|
70
|
+
if (trails)
|
|
71
|
+
trails.push(trail);
|
|
72
|
+
else
|
|
73
|
+
this.links.set(id, [trail]);
|
|
74
|
+
};
|
|
75
|
+
const index = (record, keys, trail) => {
|
|
76
|
+
if (typeof record.id !== "string")
|
|
64
77
|
return;
|
|
65
|
-
if (
|
|
78
|
+
if (isReferenceOnly(keys))
|
|
79
|
+
link(record.id, { key: 'id', up: trail });
|
|
80
|
+
else if (!this.byId.has(record.id)) {
|
|
81
|
+
this.byId.set(record.id, record);
|
|
82
|
+
this.paths.set(record.id, trail);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const linkReferences = (record, trail) => referenceKeys.forEach(key => {
|
|
86
|
+
const ref = record[key];
|
|
87
|
+
if (typeof ref === "string")
|
|
88
|
+
link(ref, { key, up: trail });
|
|
89
|
+
else if (Array.isArray(ref))
|
|
90
|
+
ref.forEach((r, i) => {
|
|
91
|
+
if (typeof r === "string")
|
|
92
|
+
link(r, { key: i, up: { key, up: trail } });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
const traverse = (node, trail) => {
|
|
96
|
+
if (!isObject(node) || visited.has(node))
|
|
66
97
|
return;
|
|
67
98
|
visited.add(node);
|
|
68
|
-
const anyNode = node;
|
|
69
|
-
if (typeof anyNode.id === "string") {
|
|
70
|
-
if (Object.keys(anyNode).filter(k => k !== '@annotation').length === 1) {
|
|
71
|
-
// this is a reference-only object, store link
|
|
72
|
-
if (!this.links.has(anyNode.id)) {
|
|
73
|
-
this.links.set(anyNode.id, new Set());
|
|
74
|
-
}
|
|
75
|
-
this.links.get(anyNode.id).add([...path, 'id']);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
if (!this.byId.has(anyNode.id)) {
|
|
79
|
-
this.byId.set(anyNode.id, node);
|
|
80
|
-
this.paths.set(anyNode.id, [...path]);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
Object
|
|
85
|
-
.keys(anyNode)
|
|
86
|
-
.filter(k => referenceKeys.has(k))
|
|
87
|
-
.forEach(key => {
|
|
88
|
-
const ref = anyNode[key];
|
|
89
|
-
if (typeof ref === "string") {
|
|
90
|
-
if (!this.links.has(ref)) {
|
|
91
|
-
this.links.set(ref, new Set());
|
|
92
|
-
}
|
|
93
|
-
this.links.get(ref).add([...path, key]);
|
|
94
|
-
}
|
|
95
|
-
else if (Array.isArray(ref)) {
|
|
96
|
-
for (const r of ref) {
|
|
97
|
-
if (typeof r === "string") {
|
|
98
|
-
if (!this.links.has(anyNode.id)) {
|
|
99
|
-
this.links.set(anyNode.id, new Set());
|
|
100
|
-
}
|
|
101
|
-
this.links.get(anyNode.id).add([...path, key]);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
99
|
if (Array.isArray(node)) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
for (const key of Object.keys(node)) {
|
|
113
|
-
traverse(anyNode[key], [...path, key]);
|
|
114
|
-
}
|
|
100
|
+
node.forEach((item, i) => traverse(item, { key: i, up: trail }));
|
|
101
|
+
return;
|
|
115
102
|
}
|
|
103
|
+
const record = node;
|
|
104
|
+
const keys = Object.keys(record);
|
|
105
|
+
index(record, keys, trail);
|
|
106
|
+
linkReferences(record, trail);
|
|
107
|
+
keys.forEach(key => traverse(record[key], { key, up: trail }));
|
|
116
108
|
};
|
|
117
|
-
traverse(this.edition,
|
|
109
|
+
traverse(this.edition, null);
|
|
118
110
|
}
|
|
119
111
|
get(anyId) {
|
|
120
112
|
return this.byId.get(anyId);
|
|
@@ -123,10 +115,11 @@ export class EditionView {
|
|
|
123
115
|
return anyIds.map(id => this.get(id)).filter((v) => !!v);
|
|
124
116
|
}
|
|
125
117
|
getPath(anyId) {
|
|
126
|
-
|
|
118
|
+
const trail = this.paths.get(anyId);
|
|
119
|
+
return trail === undefined ? undefined : pathOf(trail);
|
|
127
120
|
}
|
|
128
121
|
linksTo(anyId) {
|
|
129
|
-
return
|
|
122
|
+
return (this.links.get(anyId) ?? []).map(pathOf);
|
|
130
123
|
}
|
|
131
124
|
travelUp(versionId, callback) {
|
|
132
125
|
const v = this.get(versionId);
|
|
@@ -137,6 +130,12 @@ export class EditionView {
|
|
|
137
130
|
this.travelUp(idOf(v.basedOn), callback);
|
|
138
131
|
}
|
|
139
132
|
}
|
|
133
|
+
/** The version and its ancestors, from the version up to the root. */
|
|
134
|
+
lineageOf(versionId) {
|
|
135
|
+
const lineage = [];
|
|
136
|
+
this.travelUp(versionId, version => lineage.push(version));
|
|
137
|
+
return lineage;
|
|
138
|
+
}
|
|
140
139
|
carriersOf(symbol) {
|
|
141
140
|
return this.getAll(idsOf(symbol.carriers));
|
|
142
141
|
}
|
|
@@ -165,32 +164,29 @@ export class EditionView {
|
|
|
165
164
|
}
|
|
166
165
|
};
|
|
167
166
|
}
|
|
167
|
+
/** Where the symbol begins, as the mean onset of its carriers, or nothing for a symbol without a place. */
|
|
168
|
+
onsetOf(symbol) {
|
|
169
|
+
const carriers = this.carriersOf(symbol);
|
|
170
|
+
return carriers.length > 0 ? mean(carriers.map(carrier => carrier.horizontal.from)) : undefined;
|
|
171
|
+
}
|
|
172
|
+
/** The symbols by onset, those without a place first; symbols at one place keep their order. */
|
|
173
|
+
inOrderOfPlace(symbols) {
|
|
174
|
+
return symbols
|
|
175
|
+
.map(symbol => ({ symbol, at: this.onsetOf(symbol) || 0 }))
|
|
176
|
+
.sort((a, b) => a.at - b.at)
|
|
177
|
+
.map(({ symbol }) => symbol);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The symbols the version shows: what it and its ancestors insert,
|
|
181
|
+
* each version's deletions striking what it or its ancestors inserted.
|
|
182
|
+
*/
|
|
168
183
|
snapshot(versionId) {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// as we travel further up, remove symbols that are
|
|
174
|
-
// deleted in the versions further down
|
|
175
|
-
const deleted = [];
|
|
176
|
-
for (const toRemove of toDelete) {
|
|
177
|
-
const index = snapshot.findIndex(s => s.id === toRemove);
|
|
178
|
-
if (index !== -1) {
|
|
179
|
-
snapshot.splice(index, 1);
|
|
180
|
-
deleted.push(toRemove);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
for (const del of deleted) {
|
|
184
|
-
toDelete.splice(toDelete.indexOf(del), 1);
|
|
185
|
-
}
|
|
186
|
-
// collect symbols that are deleted in the current version
|
|
187
|
-
toDelete.push(...s.edits.flatMap(edit => edit.delete || []));
|
|
188
|
-
});
|
|
189
|
-
return snapshot.sort((a, b) => {
|
|
190
|
-
const aDimension = this.dimensionOf(a);
|
|
191
|
-
const bDimension = this.dimensionOf(b);
|
|
192
|
-
return (aDimension?.horizontal.from || 0) - (bDimension?.horizontal.from || 0);
|
|
184
|
+
const deleted = new Set();
|
|
185
|
+
const symbols = this.lineageOf(versionId).flatMap(version => {
|
|
186
|
+
deletedBy(version).forEach(id => deleted.add(id));
|
|
187
|
+
return insertedBy(version).filter(symbol => !deleted.has(symbol.id));
|
|
193
188
|
});
|
|
189
|
+
return this.inOrderOfPlace(symbols);
|
|
194
190
|
}
|
|
195
191
|
/**
|
|
196
192
|
* Assigns a generation (depth) to every node.
|
package/lib/Emulation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MIDIControlEvents } from "midifile-ts";
|
|
2
2
|
import { idOf } from "./Assumption";
|
|
3
|
-
import { pairsAmong, placementsOf } from "./Symbol";
|
|
3
|
+
import { isPerforation, pairsAmong, placementsOf } from "./Symbol";
|
|
4
4
|
import { add, mean, mm, seconds, subtract } from "./Quantity";
|
|
5
5
|
/** The punch diameter the edition's copies report, where any of them does. */
|
|
6
6
|
const punchDiameterOf = (view) => {
|
|
@@ -83,6 +83,8 @@ const displacementsOf = (events, offsetsBetween, gap) => {
|
|
|
83
83
|
});
|
|
84
84
|
return displacements;
|
|
85
85
|
};
|
|
86
|
+
/** The earliest of the times, or the beginning of the roll where there are none. */
|
|
87
|
+
const earliestOf = (times) => times.length === 0 ? seconds(0) : times.reduce((soonest, at) => at < soonest ? at : soonest);
|
|
86
88
|
/**
|
|
87
89
|
* A version of the edition, performed: the symbols are negotiated into
|
|
88
90
|
* placed events, the reproducing system plays them, and the result goes
|
|
@@ -147,22 +149,14 @@ export class Emulation {
|
|
|
147
149
|
}
|
|
148
150
|
emulateVersion(version, view, { range, skipToFirstNote = false } = {}) {
|
|
149
151
|
this.source = version.id;
|
|
152
|
+
/** A note plays only where its onset falls in the range; expressions play throughout. */
|
|
153
|
+
const inScope = (event) => !range || event.type !== 'note' || (event.horizontal.from > range[0] && event.horizontal.from < range[1]);
|
|
150
154
|
this.negotiatedEvents =
|
|
151
155
|
view.snapshot(version.id)
|
|
152
|
-
.filter(
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!dimensions)
|
|
157
|
-
return true; // in case of doubt, include the note
|
|
158
|
-
// check if the note onset is within the specified range
|
|
159
|
-
const onset = dimensions.horizontal.from;
|
|
160
|
-
return onset > range[0] && onset < range[1];
|
|
161
|
-
}
|
|
162
|
-
return true;
|
|
163
|
-
})
|
|
164
|
-
.map((e) => view.simplifySymbol(e))
|
|
165
|
-
.filter(s => s !== null);
|
|
156
|
+
.filter(isPerforation)
|
|
157
|
+
.map(symbol => view.simplifySymbol(symbol))
|
|
158
|
+
.filter(event => event !== null)
|
|
159
|
+
.filter(inScope);
|
|
166
160
|
if (this.negotiatedEvents.length === 0) {
|
|
167
161
|
this.midiEvents = [];
|
|
168
162
|
this.curves = [];
|
|
@@ -172,7 +166,7 @@ export class Emulation {
|
|
|
172
166
|
const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view));
|
|
173
167
|
this.curves = performance.curves;
|
|
174
168
|
const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
|
|
175
|
-
const origin =
|
|
169
|
+
const origin = skipToFirstNote ? earliestOf(onsets) : seconds(0);
|
|
176
170
|
this.midiEvents = performance.events
|
|
177
171
|
.map(event => ({ ...event, at: subtract(event.at, origin) }))
|
|
178
172
|
.filter(event => event.at >= 0)
|
package/lib/Quantity.d.ts
CHANGED
|
@@ -58,4 +58,8 @@ export declare const mean: <U extends Unit>(values: readonly Quantity<U>[]) => Q
|
|
|
58
58
|
export declare const inMillimeters: (place: Pixels, dpi: number) => Millimeters;
|
|
59
59
|
export declare const inCentimeters: (length: Millimeters) => Centimeters;
|
|
60
60
|
export declare const inSeconds: (time: Milliseconds) => Seconds;
|
|
61
|
+
/** A speed as a record states it, in feet or metres per minute. */
|
|
62
|
+
export type SpeedMeasure = Measure<'ft/min'> | Measure<'m/min'>;
|
|
63
|
+
/** A speed in metres per minute, whichever unit it was stated in. */
|
|
64
|
+
export declare const inMetersPerMinute: (speed: SpeedMeasure) => MetersPerMinute;
|
|
61
65
|
export {};
|
package/lib/Quantity.js
CHANGED
|
@@ -23,3 +23,6 @@ const MM_PER_INCH = 25.4;
|
|
|
23
23
|
export const inMillimeters = (place, dpi) => mm(place / dpi * MM_PER_INCH);
|
|
24
24
|
export const inCentimeters = (length) => cm(length / 10);
|
|
25
25
|
export const inSeconds = (time) => seconds(time / 1000);
|
|
26
|
+
const METERS_PER_FOOT = 0.3048;
|
|
27
|
+
/** A speed in metres per minute, whichever unit it was stated in. */
|
|
28
|
+
export const inMetersPerMinute = (speed) => speed.unit === 'm/min' ? speed.value : metersPerMinute(speed.value * METERS_PER_FOOT);
|
package/lib/RollCopy.d.ts
CHANGED
|
@@ -51,9 +51,28 @@ export interface Margins<U extends 'px' | 'mm'> {
|
|
|
51
51
|
bass: Quantity<U>;
|
|
52
52
|
unit: U;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A paper speed, as a roll's label, its catalogue or its format
|
|
56
|
+
* states it.
|
|
57
|
+
* @see crm:E54 Dimension
|
|
58
|
+
*/
|
|
59
|
+
export type PaperSpeed = Measure<'ft/min'> | Measure<'m/min'>;
|
|
60
|
+
/**
|
|
61
|
+
* What the scale an alignment found is put down to: the paper of the
|
|
62
|
+
* copy having stretched or shrunk, or the copy having been cut for
|
|
63
|
+
* another paper speed than the roll it is aligned with.
|
|
64
|
+
*/
|
|
65
|
+
export type ScaleReading = {
|
|
66
|
+
cause: 'paper';
|
|
67
|
+
condition: ObjectAssumption<PaperStretch>;
|
|
68
|
+
} | {
|
|
69
|
+
cause: 'speed';
|
|
70
|
+
speed: ObjectAssumption<PaperSpeed>;
|
|
71
|
+
};
|
|
54
72
|
/**
|
|
55
73
|
* Describes the production of a roll copy: the manufacturer,
|
|
56
|
-
* the paper used, and the
|
|
74
|
+
* the paper used, the date, and the system and paper speed the
|
|
75
|
+
* copy was cut for.
|
|
57
76
|
* @see lrmoo:F32 Item Production Event
|
|
58
77
|
*/
|
|
59
78
|
export interface ProductionEvent {
|
|
@@ -73,6 +92,22 @@ export interface ProductionEvent {
|
|
|
73
92
|
* @see dcterms:date
|
|
74
93
|
*/
|
|
75
94
|
date?: DateAssignment;
|
|
95
|
+
/**
|
|
96
|
+
* The reproducing system the copy was cut for. Left out, it is
|
|
97
|
+
* the roll's own; a Licensee re-cut of a T-100 roll names the
|
|
98
|
+
* Licensee here. A system the type vocabulary knows carries the
|
|
99
|
+
* IRI of its concept as `id`.
|
|
100
|
+
* @see crm:P32 used general technique
|
|
101
|
+
*/
|
|
102
|
+
system?: Concept;
|
|
103
|
+
/**
|
|
104
|
+
* The paper speed the copy was cut for. A copy cut from the same
|
|
105
|
+
* master for another speed comes out longer or shorter than the
|
|
106
|
+
* roll it is aligned with by the ratio of the speeds, which is
|
|
107
|
+
* what the alignment then measures.
|
|
108
|
+
* @see reo:paperSpeed
|
|
109
|
+
*/
|
|
110
|
+
speed?: ObjectAssumption<PaperSpeed>;
|
|
76
111
|
}
|
|
77
112
|
/**
|
|
78
113
|
* This type denotes identifiable activities that modified
|
|
@@ -179,6 +214,13 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
|
|
|
179
214
|
* Not exported to RDF.
|
|
180
215
|
*/
|
|
181
216
|
shift: Shift;
|
|
217
|
+
/**
|
|
218
|
+
* The factor this copy's features were scaled by to align them
|
|
219
|
+
* with the others. What it is put down to is stated apart: a
|
|
220
|
+
* paper-stretch condition, or the speed the copy was cut for.
|
|
221
|
+
* Not exported to RDF.
|
|
222
|
+
*/
|
|
223
|
+
scale: number;
|
|
182
224
|
/**
|
|
183
225
|
* Relates this copy's scan to the tracker bar: how the scanning
|
|
184
226
|
* software's hole numbering was shifted onto the bar, and where
|
package/lib/TrackerBar.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Concept } from "./Agent";
|
|
2
2
|
import { Expression, Note } from "./Symbol";
|
|
3
|
-
import { Millimeters, Track } from "./Quantity";
|
|
3
|
+
import { Millimeters, SpeedMeasure, Track } from "./Quantity";
|
|
4
4
|
/**
|
|
5
5
|
* What a tracker bar position does: sound a note, or operate one of
|
|
6
6
|
* the expression valves on the bass or the treble side.
|
|
@@ -50,6 +50,12 @@ export interface TrackerBar {
|
|
|
50
50
|
* a scan against the bar.
|
|
51
51
|
*/
|
|
52
52
|
readonly rewindTrack: Track;
|
|
53
|
+
/**
|
|
54
|
+
* The paper speed the system runs its rolls at, where the
|
|
55
|
+
* literature states one. A system whose rolls each carry a tempo
|
|
56
|
+
* of their own, as the Licensee's do, states none.
|
|
57
|
+
*/
|
|
58
|
+
readonly paperSpeed?: SpeedMeasure;
|
|
53
59
|
/** `undefined` for a position the bar does not read. */
|
|
54
60
|
meaningOf(position: Track): TrackMeaning | undefined;
|
|
55
61
|
/** `undefined` for a position the bar does not read. */
|
|
@@ -77,5 +83,14 @@ export interface TrackerBarSpec {
|
|
|
77
83
|
};
|
|
78
84
|
/** Every position outside the note block, keyed by track. */
|
|
79
85
|
expressions: ReadonlyMap<number, string>;
|
|
86
|
+
/** The speed the system runs its rolls at, where the literature states one. */
|
|
87
|
+
paperSpeed?: SpeedMeasure;
|
|
80
88
|
}
|
|
81
89
|
export declare const describeTrackerBar: (spec: TrackerBarSpec) => TrackerBar;
|
|
90
|
+
/**
|
|
91
|
+
* Puts a position of one bar onto the position of another that reads
|
|
92
|
+
* the same thing, or nowhere when the other bar does not read it. This
|
|
93
|
+
* is how a copy cut for one system takes its place in an edition of
|
|
94
|
+
* another, as a Licensee re-cut does in an edition of a T-100 roll.
|
|
95
|
+
*/
|
|
96
|
+
export declare const translationBetween: (from: TrackerBar, to: TrackerBar) => (position: Track) => Track | undefined;
|
package/lib/TrackerBar.js
CHANGED
|
@@ -41,7 +41,28 @@ export const describeTrackerBar = (spec) => {
|
|
|
41
41
|
areas,
|
|
42
42
|
expressionTypes: [...new Set(spec.expressions.values())],
|
|
43
43
|
rewindTrack: track(rewind),
|
|
44
|
+
...(spec.paperSpeed && { paperSpeed: spec.paperSpeed }),
|
|
44
45
|
meaningOf,
|
|
45
46
|
roleOf
|
|
46
47
|
};
|
|
47
48
|
};
|
|
49
|
+
const keyOf = (meaning) => meaning.type === 'note' ? `note ${meaning.pitch}` : `${meaning.scope} ${meaning.expressionType}`;
|
|
50
|
+
const positionsOf = (bar) => Array.from({ length: bar.trackCount }, (_, i) => track(i + 1));
|
|
51
|
+
/**
|
|
52
|
+
* Puts a position of one bar onto the position of another that reads
|
|
53
|
+
* the same thing, or nowhere when the other bar does not read it. This
|
|
54
|
+
* is how a copy cut for one system takes its place in an edition of
|
|
55
|
+
* another, as a Licensee re-cut does in an edition of a T-100 roll.
|
|
56
|
+
*/
|
|
57
|
+
export const translationBetween = (from, to) => {
|
|
58
|
+
const positions = new Map();
|
|
59
|
+
positionsOf(to).forEach(position => {
|
|
60
|
+
const meaning = to.meaningOf(position);
|
|
61
|
+
if (meaning)
|
|
62
|
+
positions.set(keyOf(meaning), position);
|
|
63
|
+
});
|
|
64
|
+
return position => {
|
|
65
|
+
const meaning = from.meaningOf(position);
|
|
66
|
+
return meaning ? positions.get(keyOf(meaning)) : undefined;
|
|
67
|
+
};
|
|
68
|
+
};
|
package/lib/Version.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Edit } from "./Edit";
|
|
2
2
|
import { ReferenceAssumption } from "./Assumption";
|
|
3
|
+
import { AnySymbol } from "./Symbol";
|
|
3
4
|
import { WithId, WithNote, WithType } from "./utils";
|
|
4
5
|
export declare const versionTypes: readonly [
|
|
5
6
|
/**
|
|
@@ -56,3 +57,7 @@ export interface Version extends WithId, WithType<'Version'> {
|
|
|
56
57
|
*/
|
|
57
58
|
motivations: Motivation[];
|
|
58
59
|
}
|
|
60
|
+
/** The symbols the version's edits insert. */
|
|
61
|
+
export declare const insertedBy: (version: Readonly<Version>) => AnySymbol[];
|
|
62
|
+
/** The ids of the symbols the version's edits delete. */
|
|
63
|
+
export declare const deletedBy: (version: Readonly<Version>) => string[];
|
package/lib/Version.js
CHANGED
|
@@ -9,3 +9,7 @@ export const versionTypes = [
|
|
|
9
9
|
*/
|
|
10
10
|
'unicum'
|
|
11
11
|
];
|
|
12
|
+
/** The symbols the version's edits insert. */
|
|
13
|
+
export const insertedBy = (version) => version.edits.flatMap(edit => edit.insert ?? []);
|
|
14
|
+
/** The ids of the symbols the version's edits delete. */
|
|
15
|
+
export const deletedBy = (version) => version.edits.flatMap(edit => edit.delete ?? []);
|
package/lib/alignment.d.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { ObjectAssumption } from "./Assumption";
|
|
2
1
|
import { AnyFeature } from "./Feature";
|
|
3
|
-
import {
|
|
2
|
+
import { RollCopy, Shift } from "./RollCopy";
|
|
4
3
|
import { TrackerBar } from "./TrackerBar";
|
|
5
4
|
import { Millimeters } from "./Quantity";
|
|
6
5
|
export declare const applyShift: (shift: Shift, copy: RollCopy) => void;
|
|
7
|
-
|
|
6
|
+
/** Scales the copy's features away from the beginning of the roll, and records the factor. */
|
|
7
|
+
export declare const applyScale: (factor: number, copy: RollCopy) => void;
|
|
8
8
|
/** Takes the shift off the copy's features again, as far as one was applied. */
|
|
9
9
|
export declare const revertShift: (copy: RollCopy) => void;
|
|
10
|
-
/** Takes the
|
|
11
|
-
export declare const
|
|
10
|
+
/** Takes the scale off the copy's features again, as far as one was applied. */
|
|
11
|
+
export declare const revertScale: (copy: RollCopy) => void;
|
|
12
12
|
type AlignmentResult = {
|
|
13
|
-
/** Applied before the
|
|
13
|
+
/** Applied before the scale. */
|
|
14
14
|
shift: Millimeters;
|
|
15
|
-
|
|
15
|
+
scale: number;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
18
|
* Align two rolls by computing independent linear fits of each roll's note-onset positions
|
|
19
|
-
* using only the first and last segments, then deriving a transform x2 = (x1 + shift) *
|
|
19
|
+
* using only the first and last segments, then deriving a transform x2 = (x1 + shift) * scale.
|
|
20
20
|
*/
|
|
21
21
|
export declare function alignFeatures(rollA: AnyFeature[], rollB: AnyFeature[], bar?: TrackerBar): AlignmentResult;
|
|
22
22
|
export {};
|
package/lib/alignment.js
CHANGED
|
@@ -23,12 +23,13 @@ export const applyShift = (shift, copy) => {
|
|
|
23
23
|
copy.ops = [...copy.ops, 'shifted'];
|
|
24
24
|
copy.measurements.shift = shift;
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
/** Scales the copy's features away from the beginning of the roll, and records the factor. */
|
|
27
|
+
export const applyScale = (factor, copy) => {
|
|
27
28
|
if (copy.ops.includes('stretched'))
|
|
28
29
|
return;
|
|
29
|
-
copy.features.forEach(feature => stretch(feature.horizontal,
|
|
30
|
+
copy.features.forEach(feature => stretch(feature.horizontal, factor));
|
|
30
31
|
copy.ops = [...copy.ops, 'stretched'];
|
|
31
|
-
copy.
|
|
32
|
+
copy.measurements.scale = factor;
|
|
32
33
|
};
|
|
33
34
|
/** Takes the shift off the copy's features again, as far as one was applied. */
|
|
34
35
|
export const revertShift = (copy) => {
|
|
@@ -43,15 +44,14 @@ export const revertShift = (copy) => {
|
|
|
43
44
|
copy.ops = copy.ops.filter(op => op !== 'shifted');
|
|
44
45
|
delete copy.measurements.shift;
|
|
45
46
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!copy.ops.includes('stretched') || !applied)
|
|
47
|
+
/** Takes the scale off the copy's features again, as far as one was applied. */
|
|
48
|
+
export const revertScale = (copy) => {
|
|
49
|
+
const factor = copy.measurements.scale;
|
|
50
|
+
if (!copy.ops.includes('stretched') || factor === undefined)
|
|
51
51
|
return;
|
|
52
|
-
copy.features.forEach(feature => stretch(feature.horizontal, 1 /
|
|
52
|
+
copy.features.forEach(feature => stretch(feature.horizontal, 1 / factor));
|
|
53
53
|
copy.ops = copy.ops.filter(op => op !== 'stretched');
|
|
54
|
-
|
|
54
|
+
delete copy.measurements.scale;
|
|
55
55
|
};
|
|
56
56
|
const isNoteOn = (bar) => (feature) => {
|
|
57
57
|
return feature.type === 'Hole' && bar.meaningOf(feature.vertical.from)?.type === 'note';
|
|
@@ -85,7 +85,7 @@ function selectEnds(arr, count) {
|
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
87
|
* Align two rolls by computing independent linear fits of each roll's note-onset positions
|
|
88
|
-
* using only the first and last segments, then deriving a transform x2 = (x1 + shift) *
|
|
88
|
+
* using only the first and last segments, then deriving a transform x2 = (x1 + shift) * scale.
|
|
89
89
|
*/
|
|
90
90
|
export function alignFeatures(rollA, rollB, bar = welteT100) {
|
|
91
91
|
// 1. Extract note-onset positions
|
|
@@ -102,8 +102,8 @@ export function alignFeatures(rollA, rollB, bar = welteT100) {
|
|
|
102
102
|
// 4. Fit index->position for each roll on selected ends
|
|
103
103
|
const { alpha: alphaA, beta: betaA } = fitIndexToPosition(idxA, XA);
|
|
104
104
|
const { alpha: alphaB, beta: betaB } = fitIndexToPosition(idxB, XB);
|
|
105
|
-
// 5. Derive
|
|
106
|
-
const
|
|
107
|
-
const shift = mm(betaB /
|
|
108
|
-
return {
|
|
105
|
+
// 5. Derive scale and shift such that x2 = (x1 + shift) * scale
|
|
106
|
+
const scale = alphaB / alphaA;
|
|
107
|
+
const shift = mm(betaB / scale - betaA);
|
|
108
|
+
return { scale, shift };
|
|
109
109
|
}
|