linked-rolls 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -1
- package/lib/Assumption.d.ts +124 -0
- package/lib/Assumption.js +34 -0
- package/lib/Collation.d.ts +11 -0
- package/lib/ConditionState.d.ts +8 -3
- package/lib/ConditionState.js +0 -5
- package/lib/Edit.d.ts +44 -14
- package/lib/Edit.js +2 -95
- package/lib/Edition.d.ts +144 -4
- package/lib/EditionView.d.ts +51 -0
- package/lib/EditionView.js +294 -0
- package/lib/Emulation.d.ts +21 -68
- package/lib/Emulation.js +94 -386
- package/lib/Feature.d.ts +137 -16
- package/lib/Feature.js +9 -1
- package/lib/Plan.d.ts +190 -0
- package/lib/Plan.js +555 -0
- package/lib/ReproducingSystem.d.ts +80 -0
- package/lib/RollCopy.d.ts +265 -24
- package/lib/RollCopy.js +276 -215
- package/lib/Symbol.d.ts +70 -37
- package/lib/Symbol.js +2 -27
- package/lib/TrackCalibration.d.ts +33 -0
- package/lib/TrackCalibration.js +14 -0
- package/lib/TrackerBar.d.ts +52 -5
- package/lib/TrackerBar.js +70 -49
- package/lib/Version.d.ts +32 -33
- package/lib/Version.js +3 -209
- package/lib/alignFeatures.d.ts +3 -2
- package/lib/alignFeatures.js +5 -4
- package/lib/asJsonLd.d.ts +1 -1
- package/lib/asJsonLd.js +13 -26
- package/lib/importJsonLd.d.ts +0 -1
- package/lib/importJsonLd.js +9 -72
- package/lib/index.d.ts +7 -3
- package/lib/index.js +7 -3
- package/lib/schema.json +1739 -0
- package/lib/spec/context.json +125 -144
- package/lib/systems/welteT100.d.ts +55 -0
- package/lib/systems/welteT100.js +191 -0
- package/lib/utils.d.ts +29 -0
- package/lib/validate.d.ts +3 -0
- package/lib/validate.js +10 -0
- package/package.json +41 -8
- package/lib/Condition.d.ts +0 -10
- package/lib/EditorialAssumption.d.ts +0 -67
- package/lib/EditorialAssumption.js +0 -26
- package/lib/Measurement.d.ts +0 -9
- package/lib/PlaceTimeConversion.d.ts +0 -65
- package/lib/PlaceTimeConversion.js +0 -175
- package/lib/RollEvent.d.ts +0 -76
- package/lib/RollEvent.js +0 -3
- package/lib/Stage.d.ts +0 -37
- package/lib/Stage.js +0 -165
- package/lib/Transcription.d.ts +0 -7
- package/lib/Transcription.js +0 -9
- package/lib/WithId.d.ts +0 -3
- package/lib/WithId.js +0 -1
- package/lib/alignRolls.d.ts +0 -7
- package/lib/alignRolls.js +0 -49
- package/lib/alignSymbols.d.ts +0 -7
- package/lib/alignSymbols.js +0 -49
- package/lib/aton/AtonParser.test.d.ts +0 -1
- package/lib/aton/AtonParser.test.js +0 -16
- package/lib/build-schema.cjs +0 -113
- /package/lib/{Condition.js → ReproducingSystem.js} +0 -0
- /package/lib/{Measurement.js → utils.js} +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { idOf, idsOf } from "./Assumption";
|
|
2
|
+
export const getAt = (path, obj) => {
|
|
3
|
+
let node = obj;
|
|
4
|
+
for (const key of path) {
|
|
5
|
+
if (node == null)
|
|
6
|
+
return undefined;
|
|
7
|
+
node = node[key];
|
|
8
|
+
}
|
|
9
|
+
return node;
|
|
10
|
+
};
|
|
11
|
+
const referenceKeys = new Set([
|
|
12
|
+
'delete',
|
|
13
|
+
'comprehends',
|
|
14
|
+
'motivation'
|
|
15
|
+
]);
|
|
16
|
+
export class EditionView {
|
|
17
|
+
/**
|
|
18
|
+
* Dimensions cache: one frequent operation is to find the average
|
|
19
|
+
* dimensions of a symbol based on its carriers. This cache stores
|
|
20
|
+
* the computed dimensions for reuse.
|
|
21
|
+
*/
|
|
22
|
+
//private readonly dimensionsCache: Map<string, { horizontal: HorizontalSpan, vertical: VerticalSpan }> = new Map()
|
|
23
|
+
constructor(edition) {
|
|
24
|
+
Object.defineProperty(this, "edition", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: void 0
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Map from id to object
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(this, "byId", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: new Map()
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Map from id to its path within the edition
|
|
41
|
+
*/
|
|
42
|
+
Object.defineProperty(this, "paths", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: new Map()
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Map from id to paths where it is referenced
|
|
50
|
+
*/
|
|
51
|
+
Object.defineProperty(this, "links", {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
writable: true,
|
|
55
|
+
value: new Map()
|
|
56
|
+
});
|
|
57
|
+
this.edition = edition;
|
|
58
|
+
this.indexObjects();
|
|
59
|
+
//this.buildDimensionsCache()
|
|
60
|
+
}
|
|
61
|
+
// private buildDimensionsCache() {
|
|
62
|
+
// for (const version of this.edition.versions) {
|
|
63
|
+
// for (const symbol of version.edits.flatMap(edit => edit.insert || [])) {
|
|
64
|
+
// const dim = this.dimensionOf(symbol)
|
|
65
|
+
// if (dim) {
|
|
66
|
+
// this.dimensionsCache.set(symbol.id, dim)
|
|
67
|
+
// }
|
|
68
|
+
// }
|
|
69
|
+
// }
|
|
70
|
+
// }
|
|
71
|
+
atPath(path) {
|
|
72
|
+
const node = getAt(path, this.edition);
|
|
73
|
+
return node || null;
|
|
74
|
+
}
|
|
75
|
+
indexObjects() {
|
|
76
|
+
const visited = new WeakSet();
|
|
77
|
+
const isObject = (v) => v !== null && typeof v === "object";
|
|
78
|
+
const traverse = (node, path) => {
|
|
79
|
+
if (!isObject(node))
|
|
80
|
+
return;
|
|
81
|
+
if (visited.has(node))
|
|
82
|
+
return;
|
|
83
|
+
visited.add(node);
|
|
84
|
+
const anyNode = node;
|
|
85
|
+
if (typeof anyNode.id === "string") {
|
|
86
|
+
if (Object.keys(anyNode).filter(k => k !== '@annotation').length === 1) {
|
|
87
|
+
// this is a reference-only object, store link
|
|
88
|
+
if (!this.links.has(anyNode.id)) {
|
|
89
|
+
this.links.set(anyNode.id, new Set());
|
|
90
|
+
}
|
|
91
|
+
this.links.get(anyNode.id).add([...path, 'id']);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
if (!this.byId.has(anyNode.id)) {
|
|
95
|
+
this.byId.set(anyNode.id, node);
|
|
96
|
+
this.paths.set(anyNode.id, [...path]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
Object
|
|
101
|
+
.keys(anyNode)
|
|
102
|
+
.filter(k => referenceKeys.has(k))
|
|
103
|
+
.forEach(key => {
|
|
104
|
+
const ref = anyNode[key];
|
|
105
|
+
if (typeof ref === "string") {
|
|
106
|
+
if (!this.links.has(ref)) {
|
|
107
|
+
this.links.set(ref, new Set());
|
|
108
|
+
}
|
|
109
|
+
this.links.get(ref).add([...path, key]);
|
|
110
|
+
}
|
|
111
|
+
else if (Array.isArray(ref)) {
|
|
112
|
+
for (const r of ref) {
|
|
113
|
+
if (typeof r === "string") {
|
|
114
|
+
if (!this.links.has(anyNode.id)) {
|
|
115
|
+
this.links.set(anyNode.id, new Set());
|
|
116
|
+
}
|
|
117
|
+
this.links.get(anyNode.id).add([...path, key]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (Array.isArray(node)) {
|
|
123
|
+
for (let i = 0; i < node.length; i++) {
|
|
124
|
+
traverse(node[i], [...path, i]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
for (const key of Object.keys(node)) {
|
|
129
|
+
traverse(anyNode[key], [...path, key]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
traverse(this.edition, []);
|
|
134
|
+
}
|
|
135
|
+
get(anyId) {
|
|
136
|
+
return this.byId.get(anyId);
|
|
137
|
+
}
|
|
138
|
+
getAll(anyIds) {
|
|
139
|
+
return anyIds.map(id => this.get(id)).filter((v) => !!v);
|
|
140
|
+
}
|
|
141
|
+
getPath(anyId) {
|
|
142
|
+
return this.paths.get(anyId);
|
|
143
|
+
}
|
|
144
|
+
linksTo(anyId) {
|
|
145
|
+
return Array.from(this.links.get(anyId) || []);
|
|
146
|
+
}
|
|
147
|
+
travelUp(versionId, callback) {
|
|
148
|
+
const v = this.get(versionId);
|
|
149
|
+
if (!v)
|
|
150
|
+
return;
|
|
151
|
+
callback(v);
|
|
152
|
+
if (v.basedOn) {
|
|
153
|
+
this.travelUp(idOf(v.basedOn), callback);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
carriersOf(symbol) {
|
|
157
|
+
return this.getAll(idsOf(symbol.carriers));
|
|
158
|
+
}
|
|
159
|
+
predecessorOf(versionId) {
|
|
160
|
+
const v = this.get(versionId);
|
|
161
|
+
if (!v?.basedOn)
|
|
162
|
+
return;
|
|
163
|
+
return this.get(idOf(v.basedOn));
|
|
164
|
+
}
|
|
165
|
+
dimensionOf(symbol) {
|
|
166
|
+
// if (this.dimensionsCache.has(symbol.id)) {
|
|
167
|
+
// return this.dimensionsCache.get(symbol.id);
|
|
168
|
+
// }
|
|
169
|
+
const carriers = this.getAll(idsOf(symbol.carriers));
|
|
170
|
+
if (carriers.length === 0) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const horizontalFrom = carriers.reduce((hAcc, h) => hAcc + h.horizontal.from, 0) / carriers.length;
|
|
174
|
+
const horizontalTo = carriers.reduce((hAcc, h) => hAcc + h.horizontal.to, 0) / carriers.length;
|
|
175
|
+
const verticalFrom = carriers.reduce((vAcc, v) => vAcc + v.vertical.from, 0) / carriers.length;
|
|
176
|
+
const definedVerticalTo = carriers.filter(v => v.vertical.to !== undefined);
|
|
177
|
+
let verticalTo = undefined;
|
|
178
|
+
if (definedVerticalTo.length > 0) {
|
|
179
|
+
verticalTo = definedVerticalTo
|
|
180
|
+
.reduce((vAcc, v) => vAcc + v.vertical.to, 0) / definedVerticalTo.length;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
horizontal: { unit: 'mm', from: horizontalFrom, to: horizontalTo },
|
|
184
|
+
vertical: { unit: 'track', from: verticalFrom, to: verticalTo }
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
snapshot(versionId) {
|
|
188
|
+
const snapshot = [];
|
|
189
|
+
const toDelete = [];
|
|
190
|
+
this.travelUp(versionId, s => {
|
|
191
|
+
snapshot.push(...s.edits.flatMap(edit => edit.insert || []));
|
|
192
|
+
// as we travel further up, remove symbols that are
|
|
193
|
+
// deleted in the versions further down
|
|
194
|
+
const deleted = [];
|
|
195
|
+
for (const toRemove of toDelete) {
|
|
196
|
+
const index = snapshot.findIndex(s => s.id === toRemove);
|
|
197
|
+
if (index !== -1) {
|
|
198
|
+
snapshot.splice(index, 1);
|
|
199
|
+
deleted.push(toRemove);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const del of deleted) {
|
|
203
|
+
toDelete.splice(toDelete.indexOf(del), 1);
|
|
204
|
+
}
|
|
205
|
+
// collect symbols that are deleted in the current version
|
|
206
|
+
toDelete.push(...s.edits.flatMap(edit => edit.delete || []));
|
|
207
|
+
});
|
|
208
|
+
return snapshot.sort((a, b) => {
|
|
209
|
+
const aDimension = this.dimensionOf(a);
|
|
210
|
+
const bDimension = this.dimensionOf(b);
|
|
211
|
+
return (aDimension?.horizontal.from || 0) - (bDimension?.horizontal.from || 0);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
isCollatable(symbolA, symbolB, tolerance = {
|
|
215
|
+
toleranceEnd: 5,
|
|
216
|
+
toleranceStart: 5
|
|
217
|
+
}) {
|
|
218
|
+
// two symbols are collatible if they share the same
|
|
219
|
+
// symbol characteristics (pitch, expression type etc.)
|
|
220
|
+
// and occur in the same horizontal position.
|
|
221
|
+
if (symbolA.type === 'note' && symbolB.type === 'note') {
|
|
222
|
+
if (symbolA.pitch !== symbolB.pitch)
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
else if (symbolA.type === 'expression' && symbolB.type === 'expression') {
|
|
226
|
+
if (symbolA.expressionType !== symbolB.expressionType)
|
|
227
|
+
return false;
|
|
228
|
+
if (symbolA.scope !== symbolB.scope)
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const dimensionA = this.dimensionOf(symbolA);
|
|
232
|
+
const dimensionB = this.dimensionOf(symbolB);
|
|
233
|
+
if (!dimensionA || !dimensionB)
|
|
234
|
+
return false;
|
|
235
|
+
const distanceStart = Math.abs(dimensionA.horizontal.from - dimensionB.horizontal.from);
|
|
236
|
+
const distanceEnd = Math.abs(dimensionA.horizontal.to - dimensionB.horizontal.to);
|
|
237
|
+
if (distanceStart > tolerance.toleranceStart
|
|
238
|
+
|| distanceEnd > tolerance.toleranceEnd) {
|
|
239
|
+
// the symbols are too far apart to be collated
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Assigns a generation (depth) to every node.
|
|
246
|
+
*/
|
|
247
|
+
withGenerations() {
|
|
248
|
+
const memo = new Map();
|
|
249
|
+
const inStack = new Set();
|
|
250
|
+
const computeGeneration = (id) => {
|
|
251
|
+
if (memo.has(id))
|
|
252
|
+
return memo.get(id);
|
|
253
|
+
if (inStack.has(id)) {
|
|
254
|
+
throw new Error(`Cycle detected involving node '${id}'. Check parentId links.`);
|
|
255
|
+
}
|
|
256
|
+
const node = this.get(id);
|
|
257
|
+
if (!node) {
|
|
258
|
+
memo.set(id, 0);
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
inStack.add(id);
|
|
262
|
+
let gen;
|
|
263
|
+
const basedOn = node.basedOn && idOf(node.basedOn);
|
|
264
|
+
if (basedOn === undefined) {
|
|
265
|
+
gen = 0; // root
|
|
266
|
+
}
|
|
267
|
+
else if (!this.get(basedOn)) {
|
|
268
|
+
// Orphaned parent reference — treat boundary as root
|
|
269
|
+
gen = 0;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
gen = 1 + computeGeneration(basedOn);
|
|
273
|
+
}
|
|
274
|
+
inStack.delete(id);
|
|
275
|
+
memo.set(id, gen);
|
|
276
|
+
return gen;
|
|
277
|
+
};
|
|
278
|
+
// Compute for all nodes (order doesn’t matter)
|
|
279
|
+
const withGen = this.edition.versions.map(n => ({
|
|
280
|
+
...n,
|
|
281
|
+
generation: computeGeneration(n.id),
|
|
282
|
+
}));
|
|
283
|
+
return withGen;
|
|
284
|
+
}
|
|
285
|
+
simplifySymbol(symbol) {
|
|
286
|
+
const dim = this.dimensionOf(symbol);
|
|
287
|
+
if (!symbol.carriers.length || !dim)
|
|
288
|
+
return null;
|
|
289
|
+
return {
|
|
290
|
+
...symbol,
|
|
291
|
+
...dim
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
package/lib/Emulation.d.ts
CHANGED
|
@@ -1,76 +1,29 @@
|
|
|
1
1
|
import { MidiFile } from "midifile-ts";
|
|
2
|
-
import {
|
|
3
|
-
import { PlaceTimeConversion } from "./PlaceTimeConversion";
|
|
2
|
+
import { EditionView } from "./EditionView";
|
|
4
3
|
import { Version } from "./Version";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
interface PerformedNoteEvent<T> extends PerformedRollFeature<T> {
|
|
13
|
-
pitch: number;
|
|
14
|
-
velocity: number;
|
|
15
|
-
}
|
|
16
|
-
export interface PerformedNoteOnEvent extends PerformedNoteEvent<'noteOn'> {
|
|
17
|
-
}
|
|
18
|
-
export interface PerformedNoteOffEvent extends PerformedNoteEvent<'noteOff'> {
|
|
19
|
-
}
|
|
20
|
-
export interface PerformedSustainPedalOnEvent extends PerformedRollFeature<'sustainPedalOn'> {
|
|
21
|
-
}
|
|
22
|
-
export interface PerformedSustainPedalOffEvent extends PerformedRollFeature<'sustainPedalOff'> {
|
|
23
|
-
}
|
|
24
|
-
export interface PerformedSoftPedalOnEvent extends PerformedRollFeature<'softPedalOn'> {
|
|
25
|
-
}
|
|
26
|
-
export interface PerformedSoftPedalOffEvent extends PerformedRollFeature<'softPedalOff'> {
|
|
27
|
-
}
|
|
28
|
-
export type AnyPerformedRollFeature = PerformedNoteOnEvent | PerformedNoteOffEvent | PerformedSustainPedalOnEvent | PerformedSustainPedalOffEvent | PerformedSoftPedalOnEvent | PerformedSoftPedalOffEvent;
|
|
29
|
-
type AssumedPhysicalTimeSpan = {
|
|
30
|
-
assumedPhysicalTime?: [number, number];
|
|
31
|
-
};
|
|
32
|
-
export type NegotiatedEvent = Omit<Note | Expression, 'carriers'> & Pick<RollFeature, 'horizontal' | 'vertical'> & AssumedPhysicalTimeSpan;
|
|
33
|
-
export type EmulationOptions = {
|
|
34
|
-
welte_p: number;
|
|
35
|
-
welte_f: number;
|
|
36
|
-
welte_mf: number;
|
|
37
|
-
welte_loud: number;
|
|
38
|
-
slow_decay_rate: number;
|
|
39
|
-
fastC_decay_rate: number;
|
|
40
|
-
fastD_decay_rate: number;
|
|
41
|
-
trackerBarDiameter: number;
|
|
42
|
-
punchExtensionFraction: number;
|
|
43
|
-
slow_step?: number;
|
|
44
|
-
fastC_step?: number;
|
|
45
|
-
fastD_step?: number;
|
|
46
|
-
division: number;
|
|
4
|
+
import { AnyPerformedRollFeature, EmulatedCurve, NegotiatedEvent, ReproducingSystem } from "./ReproducingSystem";
|
|
5
|
+
export type EmulationScope = {
|
|
6
|
+
/** Only notes whose onset lies within this span of the roll, in mm, are played. */
|
|
7
|
+
range?: [number, number];
|
|
8
|
+
/** Start the clock at the first note rather than at the beginning of the roll. */
|
|
9
|
+
skipToFirstNote?: boolean;
|
|
47
10
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
11
|
+
/**
|
|
12
|
+
* A version of the edition, performed: the symbols are negotiated into
|
|
13
|
+
* placed events, the reproducing system plays them, and the result goes
|
|
14
|
+
* out as MIDI in which every note and pedal step is labelled with the
|
|
15
|
+
* symbol it performs.
|
|
16
|
+
*/
|
|
17
|
+
export declare class Emulation<Options extends object> {
|
|
18
|
+
readonly system: ReproducingSystem<Options>;
|
|
19
|
+
options: Options;
|
|
20
|
+
midiEvents: AnyPerformedRollFeature[];
|
|
51
21
|
negotiatedEvents: NegotiatedEvent[];
|
|
52
|
-
|
|
53
|
-
bassVelocities: number[];
|
|
54
|
-
startTempo?: number;
|
|
55
|
-
endTempo?: number;
|
|
22
|
+
curves: readonly EmulatedCurve[];
|
|
56
23
|
source?: string;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
private assignPhysicalTime;
|
|
61
|
-
private applyTrackerBarExtension;
|
|
62
|
-
private convertEventsToMIDI;
|
|
63
|
-
emulateFromRoll(events: (Note | Expression)[]): AnyPerformedRollFeature[];
|
|
64
|
-
emulateVersion(version: Version, rollTempo?: RollTempo, skipToFirstNote?: boolean): AnyPerformedRollFeature[];
|
|
65
|
-
/**
|
|
66
|
-
* The original code of the calculateVelocities() method,
|
|
67
|
-
* written by Kitty Shi and Craig Stuart Sapp,
|
|
68
|
-
* was taken from the midi2exp project (https://github.com/pianoroll/midi2exp)
|
|
69
|
-
* and adapted to the different data representation.
|
|
70
|
-
*/
|
|
71
|
-
calculateVelocities(scope: 'treble' | 'bass'): void;
|
|
72
|
-
private insertNote;
|
|
24
|
+
constructor(system: ReproducingSystem<Options>, options?: Options);
|
|
25
|
+
applyConstraints(): void;
|
|
26
|
+
emulateVersion(version: Version, view: EditionView, { range, skipToFirstNote }?: EmulationScope): AnyPerformedRollFeature[];
|
|
73
27
|
findEventsPerforming(id: string): AnyPerformedRollFeature[];
|
|
74
28
|
asMIDI(): MidiFile;
|
|
75
29
|
}
|
|
76
|
-
export {};
|