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.
Files changed (67) hide show
  1. package/README.md +48 -1
  2. package/lib/Assumption.d.ts +124 -0
  3. package/lib/Assumption.js +34 -0
  4. package/lib/Collation.d.ts +11 -0
  5. package/lib/ConditionState.d.ts +8 -3
  6. package/lib/ConditionState.js +0 -5
  7. package/lib/Edit.d.ts +44 -14
  8. package/lib/Edit.js +2 -95
  9. package/lib/Edition.d.ts +144 -4
  10. package/lib/EditionView.d.ts +51 -0
  11. package/lib/EditionView.js +294 -0
  12. package/lib/Emulation.d.ts +21 -68
  13. package/lib/Emulation.js +94 -386
  14. package/lib/Feature.d.ts +137 -16
  15. package/lib/Feature.js +9 -1
  16. package/lib/Plan.d.ts +190 -0
  17. package/lib/Plan.js +555 -0
  18. package/lib/ReproducingSystem.d.ts +80 -0
  19. package/lib/RollCopy.d.ts +259 -24
  20. package/lib/RollCopy.js +232 -215
  21. package/lib/Symbol.d.ts +70 -37
  22. package/lib/Symbol.js +2 -27
  23. package/lib/TrackCalibration.d.ts +33 -0
  24. package/lib/TrackCalibration.js +14 -0
  25. package/lib/TrackerBar.d.ts +52 -5
  26. package/lib/TrackerBar.js +70 -49
  27. package/lib/Version.d.ts +32 -33
  28. package/lib/Version.js +3 -209
  29. package/lib/alignFeatures.d.ts +3 -2
  30. package/lib/alignFeatures.js +5 -4
  31. package/lib/asJsonLd.d.ts +1 -1
  32. package/lib/asJsonLd.js +13 -26
  33. package/lib/importJsonLd.d.ts +0 -1
  34. package/lib/importJsonLd.js +9 -72
  35. package/lib/index.d.ts +7 -3
  36. package/lib/index.js +7 -3
  37. package/lib/schema.json +1739 -0
  38. package/lib/spec/context.json +125 -144
  39. package/lib/systems/welteT100.d.ts +55 -0
  40. package/lib/systems/welteT100.js +191 -0
  41. package/lib/utils.d.ts +29 -0
  42. package/lib/validate.d.ts +3 -0
  43. package/lib/validate.js +10 -0
  44. package/package.json +41 -8
  45. package/lib/Condition.d.ts +0 -10
  46. package/lib/EditorialAssumption.d.ts +0 -67
  47. package/lib/EditorialAssumption.js +0 -26
  48. package/lib/Measurement.d.ts +0 -9
  49. package/lib/PlaceTimeConversion.d.ts +0 -65
  50. package/lib/PlaceTimeConversion.js +0 -175
  51. package/lib/RollEvent.d.ts +0 -76
  52. package/lib/RollEvent.js +0 -3
  53. package/lib/Stage.d.ts +0 -37
  54. package/lib/Stage.js +0 -165
  55. package/lib/Transcription.d.ts +0 -7
  56. package/lib/Transcription.js +0 -9
  57. package/lib/WithId.d.ts +0 -3
  58. package/lib/WithId.js +0 -1
  59. package/lib/alignRolls.d.ts +0 -7
  60. package/lib/alignRolls.js +0 -49
  61. package/lib/alignSymbols.d.ts +0 -7
  62. package/lib/alignSymbols.js +0 -49
  63. package/lib/aton/AtonParser.test.d.ts +0 -1
  64. package/lib/aton/AtonParser.test.js +0 -16
  65. package/lib/build-schema.cjs +0 -113
  66. /package/lib/{Condition.js → ReproducingSystem.js} +0 -0
  67. /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 { KinematicConversion } from "./PlaceTimeConversion";
3
- import { getSnapshot } from "./Version";
4
- import { dimensionOf } from "./Symbol";
5
- function resize(arr, newSize, defaultValue) {
6
- while (newSize > arr.length)
7
- arr.push(defaultValue);
8
- }
9
- const simplifySymbol = (symbol) => {
10
- if (!symbol.carriers || !symbol.carriers.length)
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
- welte_p: 35,
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: new KinematicConversion()
28
+ value: void 0
36
29
  });
37
- Object.defineProperty(this, "midiEvents", {
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
- // sorted list of events with the negotiated assumptions already applied
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
- // stores a velocity for every millisecond
51
- Object.defineProperty(this, "trebleVelocities", {
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, "bassVelocities", {
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
- Object.defineProperty(this, "options", {
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
- findRollTempo(tempo) {
93
- if (!tempo) {
94
- this.startTempo = 104.331;
95
- this.endTempo = 104.331;
96
- return;
97
- }
98
- this.startTempo = tempo.startsWith;
99
- this.endTempo = tempo.endsWith;
100
- }
101
- assignPhysicalTime(skipToFirstNote = false) {
102
- if (this.negotiatedEvents.length === 0)
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, rollTempo, skipToFirstNote = false) {
76
+ emulateVersion(version, view, { range, skipToFirstNote = false } = {}) {
170
77
  this.source = version.id;
171
78
  this.negotiatedEvents =
172
- getSnapshot(version)
79
+ view.snapshot(version.id)
173
80
  .filter(s => s.type === 'note' || s.type === 'expression')
174
- .map(simplifySymbol)
175
- .filter(s => s !== null);
176
- this.findRollTempo(rollTempo);
177
- this.applyTrackerBarExtension();
178
- this.assignPhysicalTime(skipToFirstNote);
179
- this.calculateVelocities('treble');
180
- this.calculateVelocities('bass');
181
- this.convertEventsToMIDI();
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
- else if (event.expressionType === 'MezzoforteOff') {
237
- if (valve_mf_on) {
238
- // fill from the mezzoforte start time to here ...
239
- isMF.fill(true, valve_mf_starttime, startMs);
240
- }
241
- valve_mf_on = false;
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
- insertNote(event, pitch, velocity) {
313
- this.midiEvents.push({
314
- type: 'noteOn',
315
- performs: event,
316
- velocity,
317
- at: event.assumedPhysicalTime[0],
318
- pitch
319
- });
320
- this.midiEvents.push({
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
- let currentTime = 0;
335
- // insert metadata first
336
- events.push({
337
- type: 'meta',
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 deltaTimeMs = (event.at - currentTime) * 1000;
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
- console.log('dealing with note on', event.pitch, event.at, deltaTimeMs);
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: deltaTimeMs,
160
+ deltaTime,
396
161
  channel: 0
397
162
  });
398
163
  }
399
- else if (event.type === 'sustainPedalOn') {
400
- events.push({
401
- type: 'meta',
402
- subtype: 'text',
403
- deltaTime: deltaTimeMs,
404
- text: event.performs.id
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: 1000,
175
+ ticksPerBeat: TICKS_PER_SECOND,
468
176
  formatType: 0,
469
177
  trackCount: 1
470
178
  },