linked-rolls 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +259 -24
- package/lib/RollCopy.js +232 -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
package/lib/Emulation.js
CHANGED
|
@@ -1,365 +1,128 @@
|
|
|
1
1
|
import { MIDIControlEvents } from "midifile-ts";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return null;
|
|
12
|
-
const mean = dimensionOf(symbol);
|
|
13
|
-
return {
|
|
14
|
-
...symbol,
|
|
15
|
-
...mean
|
|
16
|
-
};
|
|
2
|
+
import { valueOf } from "./Assumption";
|
|
3
|
+
/** The punch diameter the edition's copies report, where any of them does. */
|
|
4
|
+
const punchDiameterOf = (view) => {
|
|
5
|
+
const measured = view.edition.copies
|
|
6
|
+
.map(copy => copy.measurements.punchDiameter?.value)
|
|
7
|
+
.filter((value) => value !== undefined && value > 0);
|
|
8
|
+
if (measured.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
return measured.reduce((sum, value) => sum + value, 0) / measured.length;
|
|
17
11
|
};
|
|
12
|
+
const propertiesOf = (view) => ({
|
|
13
|
+
punchDiameter: punchDiameterOf(view),
|
|
14
|
+
tempo: view.edition.tempoAdjustment
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* A version of the edition, performed: the symbols are negotiated into
|
|
18
|
+
* placed events, the reproducing system plays them, and the result goes
|
|
19
|
+
* out as MIDI in which every note and pedal step is labelled with the
|
|
20
|
+
* symbol it performs.
|
|
21
|
+
*/
|
|
18
22
|
export class Emulation {
|
|
19
|
-
constructor(options = {
|
|
20
|
-
|
|
21
|
-
welte_f: 90,
|
|
22
|
-
welte_mf: 60,
|
|
23
|
-
welte_loud: 75,
|
|
24
|
-
trackerBarDiameter: 1.413,
|
|
25
|
-
punchExtensionFraction: 0.75,
|
|
26
|
-
slow_decay_rate: 2380,
|
|
27
|
-
fastC_decay_rate: 300,
|
|
28
|
-
fastD_decay_rate: 400,
|
|
29
|
-
division: 54
|
|
30
|
-
}) {
|
|
31
|
-
Object.defineProperty(this, "placeTimeConversion", {
|
|
23
|
+
constructor(system, options = system.defaultOptions) {
|
|
24
|
+
Object.defineProperty(this, "system", {
|
|
32
25
|
enumerable: true,
|
|
33
26
|
configurable: true,
|
|
34
27
|
writable: true,
|
|
35
|
-
value:
|
|
28
|
+
value: void 0
|
|
36
29
|
});
|
|
37
|
-
Object.defineProperty(this, "
|
|
30
|
+
Object.defineProperty(this, "options", {
|
|
38
31
|
enumerable: true,
|
|
39
32
|
configurable: true,
|
|
40
33
|
writable: true,
|
|
41
|
-
value:
|
|
34
|
+
value: void 0
|
|
42
35
|
});
|
|
43
|
-
|
|
44
|
-
Object.defineProperty(this, "negotiatedEvents", {
|
|
36
|
+
Object.defineProperty(this, "midiEvents", {
|
|
45
37
|
enumerable: true,
|
|
46
38
|
configurable: true,
|
|
47
39
|
writable: true,
|
|
48
40
|
value: []
|
|
49
41
|
});
|
|
50
|
-
//
|
|
51
|
-
Object.defineProperty(this, "
|
|
42
|
+
// sorted list of events with the negotiated assumptions already applied
|
|
43
|
+
Object.defineProperty(this, "negotiatedEvents", {
|
|
52
44
|
enumerable: true,
|
|
53
45
|
configurable: true,
|
|
54
46
|
writable: true,
|
|
55
47
|
value: []
|
|
56
48
|
});
|
|
57
|
-
Object.defineProperty(this, "
|
|
49
|
+
Object.defineProperty(this, "curves", {
|
|
58
50
|
enumerable: true,
|
|
59
51
|
configurable: true,
|
|
60
52
|
writable: true,
|
|
61
53
|
value: []
|
|
62
54
|
});
|
|
63
|
-
Object.defineProperty(this, "startTempo", {
|
|
64
|
-
enumerable: true,
|
|
65
|
-
configurable: true,
|
|
66
|
-
writable: true,
|
|
67
|
-
value: void 0
|
|
68
|
-
});
|
|
69
|
-
Object.defineProperty(this, "endTempo", {
|
|
70
|
-
enumerable: true,
|
|
71
|
-
configurable: true,
|
|
72
|
-
writable: true,
|
|
73
|
-
value: void 0
|
|
74
|
-
});
|
|
75
55
|
Object.defineProperty(this, "source", {
|
|
76
56
|
enumerable: true,
|
|
77
57
|
configurable: true,
|
|
78
58
|
writable: true,
|
|
79
59
|
value: void 0
|
|
80
60
|
});
|
|
81
|
-
|
|
82
|
-
enumerable: true,
|
|
83
|
-
configurable: true,
|
|
84
|
-
writable: true,
|
|
85
|
-
value: void 0
|
|
86
|
-
});
|
|
87
|
-
options.slow_step = (options.welte_mf - options.welte_p) / options.slow_decay_rate;
|
|
88
|
-
options.fastC_step = (options.welte_mf - options.welte_p) / options.fastC_decay_rate;
|
|
89
|
-
options.fastD_step = -(options.welte_f - options.welte_p) / options.fastD_decay_rate;
|
|
61
|
+
this.system = system;
|
|
90
62
|
this.options = options;
|
|
91
63
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return;
|
|
104
|
-
const first = skipToFirstNote ? this.negotiatedEvents[0].horizontal.from : 0;
|
|
105
|
-
for (const event of this.negotiatedEvents) {
|
|
106
|
-
if (!event.assumedPhysicalTime) {
|
|
107
|
-
// convert from mm to cm and then to time
|
|
108
|
-
event.assumedPhysicalTime = [
|
|
109
|
-
this.placeTimeConversion.placeToTime((event.horizontal.from - first) / 10),
|
|
110
|
-
this.placeTimeConversion.placeToTime((event.horizontal.to - first) / 10)
|
|
111
|
-
];
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
applyTrackerBarExtension() {
|
|
116
|
-
const correction = this.options.trackerBarDiameter * this.options.punchExtensionFraction + 0.5;
|
|
117
|
-
for (const event of this.negotiatedEvents) {
|
|
118
|
-
if (event.horizontal.to) {
|
|
119
|
-
event.horizontal.to += correction;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
convertEventsToMIDI() {
|
|
124
|
-
for (const event of this.negotiatedEvents) {
|
|
125
|
-
if (event.type === 'expression') {
|
|
126
|
-
const expression = event;
|
|
127
|
-
const map = new Map([
|
|
128
|
-
['SustainPedalOn', 'sustainPedalOn'],
|
|
129
|
-
['SustainPedalOff', 'sustainPedalOff'],
|
|
130
|
-
['SoftPedalOn', 'softPedalOn'],
|
|
131
|
-
['SoftPedalOff', 'softPedalOff']
|
|
132
|
-
]);
|
|
133
|
-
if (map.has(expression.expressionType)) {
|
|
134
|
-
this.midiEvents.push({
|
|
135
|
-
type: map.get(expression.expressionType),
|
|
136
|
-
performs: event,
|
|
137
|
-
at: event.assumedPhysicalTime[0],
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
else if (event.type === 'note') {
|
|
142
|
-
const note = event;
|
|
143
|
-
// take velocity from the calculated velocity list
|
|
144
|
-
const pitch = note.pitch;
|
|
145
|
-
if (event.vertical.from >= this.options.division) {
|
|
146
|
-
this.insertNote(event, pitch, this.trebleVelocities[+(event.assumedPhysicalTime[0] * 1000).toFixed()]);
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
this.insertNote(event, pitch, this.bassVelocities[+(event.assumedPhysicalTime[0] * 1000).toFixed()]);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
this.midiEvents.sort((a, b) => a.at - b.at);
|
|
154
|
-
}
|
|
155
|
-
emulateFromRoll(events) {
|
|
156
|
-
this.startTempo = 104.331;
|
|
157
|
-
this.endTempo = 104.331;
|
|
158
|
-
this.negotiatedEvents = events
|
|
159
|
-
.map(simplifySymbol)
|
|
160
|
-
.filter(s => s !== null);
|
|
161
|
-
this.applyTrackerBarExtension();
|
|
162
|
-
this.assignPhysicalTime();
|
|
163
|
-
this.applyTrackerBarExtension();
|
|
164
|
-
this.calculateVelocities('treble');
|
|
165
|
-
this.calculateVelocities('bass');
|
|
166
|
-
this.convertEventsToMIDI();
|
|
167
|
-
return this.midiEvents;
|
|
64
|
+
applyConstraints() {
|
|
65
|
+
this.negotiatedEvents
|
|
66
|
+
.filter((e) => e.alignedWith !== undefined)
|
|
67
|
+
.forEach(e => {
|
|
68
|
+
const ref = this.negotiatedEvents.find(ev => ev.id === valueOf(e.alignedWith));
|
|
69
|
+
if (!ref)
|
|
70
|
+
return;
|
|
71
|
+
const distance = ref.horizontal.from - e.horizontal.from;
|
|
72
|
+
e.horizontal.from += distance;
|
|
73
|
+
e.horizontal.to += distance;
|
|
74
|
+
});
|
|
168
75
|
}
|
|
169
|
-
emulateVersion(version,
|
|
76
|
+
emulateVersion(version, view, { range, skipToFirstNote = false } = {}) {
|
|
170
77
|
this.source = version.id;
|
|
171
78
|
this.negotiatedEvents =
|
|
172
|
-
|
|
79
|
+
view.snapshot(version.id)
|
|
173
80
|
.filter(s => s.type === 'note' || s.type === 'expression')
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return this.midiEvents;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* The original code of the calculateVelocities() method,
|
|
186
|
-
* written by Kitty Shi and Craig Stuart Sapp,
|
|
187
|
-
* was taken from the midi2exp project (https://github.com/pianoroll/midi2exp)
|
|
188
|
-
* and adapted to the different data representation.
|
|
189
|
-
*/
|
|
190
|
-
calculateVelocities(scope) {
|
|
191
|
-
if (!this.negotiatedEvents.length)
|
|
192
|
-
return;
|
|
193
|
-
const velocities = scope === 'treble' ? this.trebleVelocities : this.bassVelocities;
|
|
194
|
-
const isMF = []; // is MF hook on?
|
|
195
|
-
const isSlowC = []; // is slow crescendo on?
|
|
196
|
-
const isFastC = []; // is fast crescendo on?
|
|
197
|
-
const isFastD = []; // is fast decrescendo on?
|
|
198
|
-
let lastOnsetMs = this.negotiatedEvents[this.negotiatedEvents.length - 1].assumedPhysicalTime[0];
|
|
199
|
-
if (!lastOnsetMs) {
|
|
200
|
-
console.log('Failed calculating the last onset.');
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
lastOnsetMs *= 1000;
|
|
204
|
-
// set all of the times to piano by default
|
|
205
|
-
resize(velocities, lastOnsetMs, this.options.welte_p);
|
|
206
|
-
// fill all hook maps with false
|
|
207
|
-
for (const target of [isMF, isSlowC, isFastC, isFastD]) {
|
|
208
|
-
resize(target, lastOnsetMs, false);
|
|
209
|
-
}
|
|
210
|
-
// Lock and Cancel
|
|
211
|
-
let valve_mf_on = false;
|
|
212
|
-
let valve_slowc_on = false;
|
|
213
|
-
let valve_mf_starttime = 0; // 0 for off
|
|
214
|
-
let valve_slowc_starttime = 0;
|
|
215
|
-
// First pass: For each time section calculate the current boolean
|
|
216
|
-
// state of each expression.
|
|
217
|
-
for (const negotiatedEvent of this.negotiatedEvents) {
|
|
218
|
-
if (negotiatedEvent.type !== 'expression')
|
|
219
|
-
continue;
|
|
220
|
-
if (scope === 'treble' && negotiatedEvent.vertical.from < this.options.division)
|
|
221
|
-
continue;
|
|
222
|
-
else if (scope === 'bass' && negotiatedEvent.vertical.from >= this.options.division)
|
|
223
|
-
continue;
|
|
224
|
-
const event = negotiatedEvent;
|
|
225
|
-
const startMs = event.assumedPhysicalTime[0] * 1000;
|
|
226
|
-
const endMs = event.assumedPhysicalTime[1] * 1000;
|
|
227
|
-
// console.log('encoutering expression', event.expressionType["@id"], 'from', startMs, 'to', endMs)
|
|
228
|
-
if (event.expressionType === 'MezzoforteOn') {
|
|
229
|
-
// update the mezzoforte start time
|
|
230
|
-
// only if the mf valve is not on already
|
|
231
|
-
if (!valve_mf_on) {
|
|
232
|
-
valve_mf_on = true;
|
|
233
|
-
valve_mf_starttime = startMs;
|
|
81
|
+
.filter(s => {
|
|
82
|
+
if (range && s.type === 'note') {
|
|
83
|
+
const dimensions = view.dimensionOf(s);
|
|
84
|
+
if (!dimensions)
|
|
85
|
+
return true; // in case of doubt, include the note
|
|
86
|
+
// check if the note onset is within the specified range
|
|
87
|
+
const onset = dimensions.horizontal.from;
|
|
88
|
+
return onset > range[0] && onset < range[1];
|
|
234
89
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
else if (event.expressionType === 'SlowCrescendoOn') {
|
|
244
|
-
// update the slow crescendo start time
|
|
245
|
-
// only if slow crescendo is not on already
|
|
246
|
-
if (!valve_slowc_on) {
|
|
247
|
-
valve_slowc_on = true;
|
|
248
|
-
valve_slowc_starttime = startMs;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
else if (event.expressionType === 'SlowCrescendoOff') {
|
|
252
|
-
if (valve_slowc_on) {
|
|
253
|
-
// fill from the mezzoforte start time to here ...
|
|
254
|
-
isSlowC.fill(true, valve_slowc_starttime, startMs);
|
|
255
|
-
}
|
|
256
|
-
valve_slowc_on = false;
|
|
257
|
-
}
|
|
258
|
-
else if (event.expressionType === 'ForzandoOn') {
|
|
259
|
-
// Forzando On/Off are a direct operations (length of perforation matters)
|
|
260
|
-
isFastC.fill(true, startMs, endMs);
|
|
261
|
-
}
|
|
262
|
-
else if (event.expressionType === 'ForzandoOff') {
|
|
263
|
-
// Forzando On/Off are a direct operations (length of perforation matters)
|
|
264
|
-
isFastD.fill(true, startMs, endMs);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// TODO: deal with the last case (if crescendo OFF is missing)
|
|
268
|
-
// Second pass, update the current velocity according to the previous one
|
|
269
|
-
let amount = 0.0;
|
|
270
|
-
let eps = 0.0001;
|
|
271
|
-
for (let i = 1; i < lastOnsetMs; i++) {
|
|
272
|
-
if (!isSlowC[i] && !isFastC[i] && !isFastD[i]) {
|
|
273
|
-
// slow decrescendo is always on
|
|
274
|
-
amount = -this.options.slow_step;
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
amount =
|
|
278
|
-
(isSlowC[i] ? this.options.slow_step : 0) +
|
|
279
|
-
(isFastC[i] ? this.options.fastC_step : 0) +
|
|
280
|
-
(isFastD[i] ? this.options.fastD_step : 0);
|
|
281
|
-
}
|
|
282
|
-
velocities[i] = velocities[i - 1] + amount;
|
|
283
|
-
if (isMF[i]) {
|
|
284
|
-
if (velocities[i - 1] > this.options.welte_mf) {
|
|
285
|
-
if (amount < 0) {
|
|
286
|
-
velocities[i] = Math.max(this.options.welte_mf + eps, velocities[i]);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
velocities[i] = Math.min(this.options.welte_f, velocities[i]);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
else if (velocities[i - 1] < this.options.welte_mf) {
|
|
293
|
-
if (amount > 0) {
|
|
294
|
-
velocities[i] = Math.min(this.options.welte_mf - eps, velocities[i]);
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
velocities[i] = Math.max(this.options.welte_p, velocities[i]);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
// slow crescendo will only reach welte_loud
|
|
303
|
-
if (isSlowC[i] && !isFastC[i] && velocities[i - 1] < this.options.welte_loud) {
|
|
304
|
-
velocities[i] = Math.min(velocities[i], this.options.welte_loud - eps);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// regulating max and min
|
|
308
|
-
velocities[i] = Math.max(this.options.welte_p, velocities[i]);
|
|
309
|
-
velocities[i] = Math.min(this.options.welte_f, velocities[i]);
|
|
90
|
+
return true;
|
|
91
|
+
})
|
|
92
|
+
.map((e) => view.simplifySymbol(e))
|
|
93
|
+
.filter(s => s !== null);
|
|
94
|
+
if (this.negotiatedEvents.length === 0) {
|
|
95
|
+
this.midiEvents = [];
|
|
96
|
+
this.curves = [];
|
|
97
|
+
return this.midiEvents;
|
|
310
98
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
this.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
at: event.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.midiEvents
|
|
321
|
-
type: 'noteOff',
|
|
322
|
-
performs: event,
|
|
323
|
-
velocity: 127,
|
|
324
|
-
at: event.assumedPhysicalTime[1],
|
|
325
|
-
pitch
|
|
326
|
-
});
|
|
99
|
+
this.applyConstraints();
|
|
100
|
+
const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view));
|
|
101
|
+
this.curves = performance.curves;
|
|
102
|
+
const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
|
|
103
|
+
const origin = skipToFirstNote && onsets.length > 0 ? Math.min(...onsets) : 0;
|
|
104
|
+
this.midiEvents = performance.events
|
|
105
|
+
.map(event => ({ ...event, at: event.at - origin }))
|
|
106
|
+
.filter(event => event.at >= 0)
|
|
107
|
+
.sort((a, b) => a.at - b.at);
|
|
108
|
+
return this.midiEvents;
|
|
327
109
|
}
|
|
328
110
|
findEventsPerforming(id) {
|
|
329
111
|
return this.midiEvents.filter(event => event.performs.id === id);
|
|
330
112
|
}
|
|
331
113
|
asMIDI() {
|
|
114
|
+
const TICKS_PER_SECOND = 1000;
|
|
332
115
|
const events = [];
|
|
333
116
|
this.midiEvents.sort((a, b) => a.at - b.at);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
subtype: 'text',
|
|
339
|
-
text: 'linked-rolls (based on midi2exp)',
|
|
340
|
-
deltaTime: 0
|
|
341
|
-
});
|
|
117
|
+
const text = (text, deltaTime = 0) => ({ type: 'meta', subtype: 'text', text, deltaTime });
|
|
118
|
+
const controller = (controllerType, value, deltaTime = 0) => ({ type: 'channel', subtype: 'controller', controllerType, value, deltaTime, channel: 0 });
|
|
119
|
+
const controllerOf = (event) => event.type === 'damper' ? MIDIControlEvents.SUSTAIN : MIDIControlEvents.SOFT_PEDAL;
|
|
120
|
+
events.push(text(`linked-rolls (${this.system.name})`));
|
|
342
121
|
if (this.source) {
|
|
343
|
-
events.push(
|
|
344
|
-
type: 'meta',
|
|
345
|
-
subtype: 'text',
|
|
346
|
-
text: this.source,
|
|
347
|
-
deltaTime: 0
|
|
348
|
-
});
|
|
122
|
+
events.push(text(this.source));
|
|
349
123
|
}
|
|
350
|
-
events.push({
|
|
351
|
-
type: 'meta',
|
|
352
|
-
subtype: 'text',
|
|
353
|
-
text: `place-time-conversion: ${this.placeTimeConversion.summary}`,
|
|
354
|
-
deltaTime: 0
|
|
355
|
-
});
|
|
356
124
|
for (const [key, value] of Object.entries(this.options)) {
|
|
357
|
-
events.push({
|
|
358
|
-
type: 'meta',
|
|
359
|
-
subtype: 'text',
|
|
360
|
-
text: `${key}=${value}`,
|
|
361
|
-
deltaTime: 0
|
|
362
|
-
});
|
|
125
|
+
events.push(text(`${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`));
|
|
363
126
|
}
|
|
364
127
|
events.push({
|
|
365
128
|
type: 'meta',
|
|
@@ -367,16 +130,18 @@ export class Emulation {
|
|
|
367
130
|
microsecondsPerBeat: 1000000,
|
|
368
131
|
deltaTime: 0
|
|
369
132
|
});
|
|
133
|
+
// both pedals start at rest
|
|
134
|
+
events.push(controller(MIDIControlEvents.SUSTAIN, 0), controller(MIDIControlEvents.SOFT_PEDAL, 0));
|
|
135
|
+
// a pedal step is labelled with its perforation only where that
|
|
136
|
+
// perforation changes, so the file is not swamped with labels
|
|
137
|
+
const lastCause = {};
|
|
138
|
+
let currentTick = 0;
|
|
370
139
|
for (const event of this.midiEvents) {
|
|
371
|
-
const
|
|
140
|
+
const tick = Math.round(event.at * TICKS_PER_SECOND);
|
|
141
|
+
const deltaTime = tick - currentTick;
|
|
142
|
+
currentTick = tick;
|
|
372
143
|
if (event.type === 'noteOn') {
|
|
373
|
-
|
|
374
|
-
events.push({
|
|
375
|
-
type: 'meta',
|
|
376
|
-
subtype: 'text',
|
|
377
|
-
deltaTime: deltaTimeMs,
|
|
378
|
-
text: event.performs.id
|
|
379
|
-
});
|
|
144
|
+
events.push(text(event.performs.id, deltaTime));
|
|
380
145
|
events.push({
|
|
381
146
|
type: 'channel',
|
|
382
147
|
subtype: 'noteOn',
|
|
@@ -392,79 +157,22 @@ export class Emulation {
|
|
|
392
157
|
subtype: 'noteOff',
|
|
393
158
|
noteNumber: event.pitch,
|
|
394
159
|
velocity: 127,
|
|
395
|
-
deltaTime
|
|
160
|
+
deltaTime,
|
|
396
161
|
channel: 0
|
|
397
162
|
});
|
|
398
163
|
}
|
|
399
|
-
else
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
deltaTime
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
events.push({
|
|
407
|
-
type: 'channel',
|
|
408
|
-
subtype: 'controller',
|
|
409
|
-
controllerType: MIDIControlEvents.SUSTAIN,
|
|
410
|
-
deltaTime: 0,
|
|
411
|
-
channel: 0,
|
|
412
|
-
value: 127
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
else if (event.type === 'sustainPedalOff') {
|
|
416
|
-
events.push({
|
|
417
|
-
type: 'meta',
|
|
418
|
-
subtype: 'text',
|
|
419
|
-
deltaTime: deltaTimeMs,
|
|
420
|
-
text: event.performs.id
|
|
421
|
-
});
|
|
422
|
-
events.push({
|
|
423
|
-
type: 'channel',
|
|
424
|
-
subtype: 'controller',
|
|
425
|
-
controllerType: MIDIControlEvents.SUSTAIN,
|
|
426
|
-
deltaTime: 0,
|
|
427
|
-
channel: 0,
|
|
428
|
-
value: 0
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
else if (event.type === 'softPedalOn') {
|
|
432
|
-
events.push({
|
|
433
|
-
type: 'meta',
|
|
434
|
-
subtype: 'text',
|
|
435
|
-
deltaTime: deltaTimeMs,
|
|
436
|
-
text: event.performs.id
|
|
437
|
-
});
|
|
438
|
-
events.push({
|
|
439
|
-
type: 'channel',
|
|
440
|
-
subtype: 'controller',
|
|
441
|
-
controllerType: MIDIControlEvents.SOFT_PEDAL,
|
|
442
|
-
deltaTime: 0,
|
|
443
|
-
channel: 0,
|
|
444
|
-
value: 127
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
else if (event.type === 'softPedalOff') {
|
|
448
|
-
events.push({
|
|
449
|
-
type: 'meta',
|
|
450
|
-
subtype: 'text',
|
|
451
|
-
deltaTime: deltaTimeMs,
|
|
452
|
-
text: event.performs.id
|
|
453
|
-
});
|
|
454
|
-
events.push({
|
|
455
|
-
type: 'channel',
|
|
456
|
-
subtype: 'controller',
|
|
457
|
-
controllerType: MIDIControlEvents.SOFT_PEDAL,
|
|
458
|
-
deltaTime: 0,
|
|
459
|
-
channel: 0,
|
|
460
|
-
value: 0
|
|
461
|
-
});
|
|
164
|
+
else {
|
|
165
|
+
const labelled = lastCause[event.type] === event.performs.id;
|
|
166
|
+
lastCause[event.type] = event.performs.id;
|
|
167
|
+
if (!labelled) {
|
|
168
|
+
events.push(text(event.performs.id, deltaTime));
|
|
169
|
+
}
|
|
170
|
+
events.push(controller(controllerOf(event), event.value, labelled ? deltaTime : 0));
|
|
462
171
|
}
|
|
463
|
-
currentTime = event.at;
|
|
464
172
|
}
|
|
465
173
|
return {
|
|
466
174
|
header: {
|
|
467
|
-
ticksPerBeat:
|
|
175
|
+
ticksPerBeat: TICKS_PER_SECOND,
|
|
468
176
|
formatType: 0,
|
|
469
177
|
trackCount: 1
|
|
470
178
|
},
|