linked-rolls 0.4.1 → 0.5.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.
Files changed (47) hide show
  1. package/lib/Agent.d.ts +59 -0
  2. package/lib/Agent.js +1 -0
  3. package/lib/Assumption.d.ts +19 -1
  4. package/lib/Collation.d.ts +19 -0
  5. package/lib/Collation.js +25 -1
  6. package/lib/Edit.d.ts +1 -8
  7. package/lib/Edition.d.ts +4 -77
  8. package/lib/Edition.js +1 -1
  9. package/lib/EditionView.d.ts +0 -7
  10. package/lib/EditionView.js +0 -50
  11. package/lib/ReproducingSystem.d.ts +23 -1
  12. package/lib/RollCopy.d.ts +2 -64
  13. package/lib/RollCopy.js +2 -270
  14. package/lib/Symbol.d.ts +2 -1
  15. package/lib/Symbol.js +1 -1
  16. package/lib/TrackerBar.d.ts +16 -14
  17. package/lib/TrackerBar.js +1 -56
  18. package/lib/alignment.d.ts +21 -0
  19. package/lib/alignment.js +117 -0
  20. package/lib/constraints.d.ts +27 -0
  21. package/lib/constraints.js +61 -0
  22. package/lib/editionOps.d.ts +76 -0
  23. package/lib/editionOps.js +312 -0
  24. package/lib/index.d.ts +19 -14
  25. package/lib/index.js +19 -14
  26. package/lib/migrate.js +2 -1
  27. package/lib/readers/spencerMidi.d.ts +26 -0
  28. package/lib/readers/spencerMidi.js +58 -0
  29. package/lib/readers/stanfordAton.d.ts +18 -0
  30. package/lib/readers/stanfordAton.js +158 -0
  31. package/lib/schema.json +2 -2
  32. package/lib/systems/welteT100/bar.d.ts +14 -0
  33. package/lib/systems/welteT100/bar.js +56 -0
  34. package/lib/systems/{welteT100.d.ts → welteT100/system.d.ts} +1 -1
  35. package/lib/systems/{welteT100.js → welteT100/system.js} +1 -1
  36. package/lib/utils.d.ts +0 -8
  37. package/lib/validate.d.ts +0 -27
  38. package/lib/validate.js +0 -61
  39. package/package.json +3 -3
  40. package/lib/Plan.d.ts +0 -198
  41. package/lib/Plan.js +0 -611
  42. package/lib/alignFeatures.d.ts +0 -12
  43. package/lib/alignFeatures.js +0 -55
  44. /package/lib/{aton → readers}/AtonParser.d.ts +0 -0
  45. /package/lib/{aton → readers}/AtonParser.js +0 -0
  46. /package/lib/{asMIDISpans.d.ts → readers/midiSpans.d.ts} +0 -0
  47. /package/lib/{asMIDISpans.js → readers/midiSpans.js} +0 -0
package/lib/Plan.js DELETED
@@ -1,611 +0,0 @@
1
- import { getAt } from "./EditionView";
2
- import { isPerforation, placementRelations } from "./Symbol";
3
- import { v4 } from "uuid";
4
- import { asSymbols } from "./RollCopy";
5
- import { assignReference, idOf } from "./Assumption";
6
- export const isPlan = (obj) => {
7
- return obj && typeof obj.build === 'function' && typeof obj.setView === 'function';
8
- };
9
- export class BasePlan {
10
- constructor() {
11
- Object.defineProperty(this, "view", {
12
- enumerable: true,
13
- configurable: true,
14
- writable: true,
15
- value: null
16
- });
17
- Object.defineProperty(this, "setView", {
18
- enumerable: true,
19
- configurable: true,
20
- writable: true,
21
- value: (view) => {
22
- this.view = view;
23
- }
24
- });
25
- }
26
- }
27
- /**
28
- * Walks through the versions. If it finds a symbol that is still
29
- * part of the tradition (i.e. it is included in the current snapshot)
30
- * and is equivalent with the given symbol, it will add the feature
31
- * carrying the symbol to the collated symbol. Otherwise, the
32
- * given symbol will be added to the current version's insertions.
33
- *
34
- * All symbols of the tradition that are not included in the given
35
- * symbols are considered to be deleted.
36
- *
37
- * @param creation
38
- * @param symbols
39
- * @returns
40
- */
41
- export class ConnectVersions extends BasePlan {
42
- constructor(childId, parentId, tolerance = {
43
- toleranceEnd: 5,
44
- toleranceStart: 5
45
- }) {
46
- super();
47
- Object.defineProperty(this, "childId", {
48
- enumerable: true,
49
- configurable: true,
50
- writable: true,
51
- value: childId
52
- });
53
- Object.defineProperty(this, "parentId", {
54
- enumerable: true,
55
- configurable: true,
56
- writable: true,
57
- value: parentId
58
- });
59
- Object.defineProperty(this, "tolerance", {
60
- enumerable: true,
61
- configurable: true,
62
- writable: true,
63
- value: tolerance
64
- });
65
- }
66
- build() {
67
- if (!this.view)
68
- return [];
69
- const view = this.view;
70
- const result = [];
71
- const parentSnapshot = view.snapshot(this.parentId);
72
- const childSnapshot = view.snapshot(this.childId);
73
- const treatedSymbols = [];
74
- // can the symbol be collated with any of the
75
- // symbols of the tradition, i.e. the ones
76
- // included in the current snapshot?
77
- const insertions = [...childSnapshot];
78
- for (const symbol of childSnapshot) {
79
- parentSnapshot
80
- .filter(toCompare => view.isCollatable(symbol, toCompare, this.tolerance))
81
- .forEach(parentSymbol => {
82
- result.push(draft => {
83
- const symbolPath = view.getPath(parentSymbol.id);
84
- if (!symbolPath)
85
- return;
86
- const correspSymbol = getAt(symbolPath, draft);
87
- if (!correspSymbol)
88
- return;
89
- correspSymbol.carriers.push(...symbol.carriers);
90
- });
91
- insertions.splice(insertions.indexOf(symbol), 1);
92
- treatedSymbols.push(parentSymbol);
93
- });
94
- }
95
- const edits = insertions.map((symbol) => {
96
- return {
97
- type: 'edit',
98
- id: v4(),
99
- insert: [symbol],
100
- delete: [],
101
- };
102
- });
103
- const deletions = parentSnapshot.filter(sym => {
104
- return !treatedSymbols.includes(sym);
105
- });
106
- for (const symbol of deletions) {
107
- edits.push({
108
- type: 'edit',
109
- id: v4(),
110
- insert: [],
111
- delete: [symbol.id],
112
- });
113
- }
114
- result.push(draft => {
115
- const v = draft.versions.find(v => v.id === this.childId);
116
- if (!v)
117
- return;
118
- v.edits = edits;
119
- v.basedOn = assignReference(this.parentId);
120
- });
121
- return result;
122
- }
123
- }
124
- /**
125
- * Walks through the versions. If it finds a symbol that is still
126
- * part of the tradition (i.e. it is included in the current snapshot)
127
- * and is equivalent with the given symbol, it will add the feature
128
- * carrying the symbol to the collated symbol. Otherwise, the
129
- * given symbol will be added to the current version's insertions.
130
- *
131
- * All symbols of the tradition that are not included in the given
132
- * symbols are considered to be deleted.
133
- *
134
- * @param creation
135
- * @param symbols
136
- * @returns
137
- */
138
- export class CreateVersion extends BasePlan {
139
- constructor(siglum, copy) {
140
- super();
141
- Object.defineProperty(this, "siglum", {
142
- enumerable: true,
143
- configurable: true,
144
- writable: true,
145
- value: siglum
146
- });
147
- Object.defineProperty(this, "copy", {
148
- enumerable: true,
149
- configurable: true,
150
- writable: true,
151
- value: copy
152
- });
153
- }
154
- build() {
155
- const view = this.view;
156
- if (!view)
157
- return [];
158
- const result = [];
159
- result.push(draft => {
160
- draft.copies.push(this.copy);
161
- const newVersion = {
162
- type: 'Version',
163
- siglum: this.siglum,
164
- id: v4(),
165
- edits: asSymbols(this.copy.features).map((symbol) => {
166
- return {
167
- type: 'edit',
168
- id: v4(),
169
- insert: [symbol],
170
- delete: [],
171
- };
172
- }),
173
- versionType: 'edition',
174
- motivations: []
175
- };
176
- draft.versions.push(newVersion);
177
- });
178
- return result;
179
- }
180
- }
181
- /**
182
- * Creates a new version based on the given version
183
- * calculating the effect of a cover on existing symbols.
184
-
185
- export class CoverPerforation extends BasePlan {
186
- constructor(
187
- private copyId: string,
188
- private versionId: string,
189
- private coverDimension: { horizontal: HorizontalSpan, vertical: VerticalSpan },
190
- private material: string | undefined = undefined,
191
- ) {
192
- super()
193
- }
194
-
195
- build(): EditionOp[] {
196
- const view = this.view
197
- if (!view) return []
198
-
199
- return [
200
- (draft: Draft<Edition>): void => {
201
- const version = draft.versions.find(v => v.id === this.versionId)
202
- const copy = draft.copies.find(c => c.id === this.copyId)
203
- if (!version || !copy) return
204
-
205
- // find perforations in the snapshot that
206
- // overlap with the cover
207
- const overlappingSymbols =
208
- view.snapshot(this.versionId)
209
- .filter(symbol => {
210
- const dimension = view.dimensionOf(symbol)
211
- if (!dimension) return false
212
-
213
- return (
214
- (symbol.type === 'note' || symbol.type === 'expression') &&
215
- overlaps(dimension, this.coverDimension)
216
- )
217
- })
218
-
219
- if (!overlappingSymbols.length) return undefined
220
-
221
- const deepClone = JSON.parse(JSON.stringify(overlappingSymbols)) as AnySymbol[]
222
-
223
- for (const symbol of deepClone) {
224
- //symbol.id = v4();
225
- const dimension = structuredClone(view.dimensionOf(symbol))
226
- if (!dimension) continue
227
-
228
- // check if the cover partially covers the beginning
229
- if (dimension.horizontal.from >= this.coverDimension.horizontal.from &&
230
- dimension.horizontal.from <= this.coverDimension.horizontal.to
231
- ) {
232
- dimension.horizontal.from = this.coverDimension.horizontal.to
233
- }
234
-
235
- // check if the cover partially covers the ending
236
- if (dimension.horizontal.to >= this.coverDimension.horizontal.from &&
237
- dimension.horizontal.to <= this.coverDimension.horizontal.to
238
- ) {
239
- dimension.horizontal.to = this.coverDimension.horizontal.from
240
- }
241
-
242
- const newFeature: RollFeature = {
243
- ...dimension,
244
- id: v4(),
245
- }
246
-
247
- copy.features.push(newFeature)
248
- symbol.id = `symbol-${v4().slice(0, 8)}`
249
- symbol.carriers = [assignReference(newFeature.id)]
250
- }
251
-
252
- const coverId = `cover-${v4().slice(0, 8)}`
253
- copy.features.push({
254
- ...this.coverDimension,
255
- id: coverId,
256
- })
257
-
258
- const edit: Edit = {
259
- type: 'edit',
260
- id: v4(),
261
- delete: overlappingSymbols.map(s => s.id),
262
- insert: deepClone,
263
- intentionOf: [{
264
- type: 'cover',
265
- id: v4(),
266
- note: this.material,
267
- carriers: [assignReference(coverId)],
268
- }]
269
- }
270
-
271
- draft.versions.push({
272
- edits: [edit],
273
- id: v4(),
274
- basedOn: assignReference(version.id),
275
- siglum: version.siglum + ' rev',
276
- type: 'authorised-revision',
277
- motivations: [
278
- {
279
- type: 'motivation',
280
- note: 'Stanzfehler korrigiert',
281
- id: v4(),
282
- }
283
- ],
284
- })
285
- }]
286
- }
287
- }
288
- */
289
- export class DetachVersion extends BasePlan {
290
- constructor(versionId) {
291
- super();
292
- Object.defineProperty(this, "versionId", {
293
- enumerable: true,
294
- configurable: true,
295
- writable: true,
296
- value: versionId
297
- });
298
- }
299
- build() {
300
- if (!this.view)
301
- return [];
302
- const snapshot = this.view.snapshot(this.versionId);
303
- const newEdits = snapshot.map((symbol) => ({
304
- type: 'edit',
305
- id: v4(),
306
- insert: [symbol],
307
- delete: [],
308
- }));
309
- return [draft => {
310
- const v = draft.versions.find(ver => ver.id === this.versionId);
311
- if (!v)
312
- return;
313
- v.edits = newEdits;
314
- delete v.basedOn;
315
- v.motivations = [];
316
- }];
317
- }
318
- }
319
- export class RemoveFeature extends BasePlan {
320
- constructor(copyID, featureIDs) {
321
- super();
322
- Object.defineProperty(this, "copyID", {
323
- enumerable: true,
324
- configurable: true,
325
- writable: true,
326
- value: copyID
327
- });
328
- Object.defineProperty(this, "featureIDs", {
329
- enumerable: true,
330
- configurable: true,
331
- writable: true,
332
- value: featureIDs
333
- });
334
- }
335
- build() {
336
- const view = this.view;
337
- if (!view)
338
- return [];
339
- return [
340
- (draft) => {
341
- const copy = draft.copies.find(c => c.id === this.copyID);
342
- if (!copy)
343
- return;
344
- copy.features = copy.features.filter(f => !this.featureIDs.includes(f.id));
345
- this.featureIDs.forEach(featureId => {
346
- // expect a path of format:
347
- // ['versions', index, 'edits', index, 'insert', index, 'carriers', index, 'id']
348
- const carrierPath = view.linksTo(featureId).at(0);
349
- if (!carrierPath)
350
- return;
351
- // expect to find the parent symbol at:
352
- // ['versions', index, 'edits', index, 'insert', index]
353
- const symbol = getAt(carrierPath.slice(0, 6), draft);
354
- if (!symbol)
355
- return;
356
- symbol.carriers = symbol.carriers.filter(c => idOf(c) !== featureId);
357
- // if the symbol has no more carriers, remove it
358
- if (symbol.carriers.length === 0) {
359
- // expect to find the parent edit at:
360
- // ['versions', index, 'edits', index]
361
- const inserts = getAt(carrierPath.slice(0, 5), draft);
362
- if (!inserts)
363
- return;
364
- inserts.splice(inserts.findIndex(s => s.id === symbol.id), 1);
365
- if (inserts.length === 0) {
366
- const edit = getAt(carrierPath.slice(0, 4), draft);
367
- if (!edit)
368
- return;
369
- if (!edit.insert?.length && !edit.delete?.length) {
370
- const edits = getAt(carrierPath.slice(0, 3), draft);
371
- if (!edits)
372
- return;
373
- edits.splice(edits.findIndex(e => e.id === edit.id), 1);
374
- }
375
- }
376
- }
377
- });
378
- }
379
- ];
380
- }
381
- }
382
- const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
383
- const insertedIn = (versions) => versions.flatMap(version => version.edits).flatMap(edit => edit.insert ?? []);
384
- /**
385
- * A symbol every carrier of which lies on the copy loses its evidence
386
- * with the copy. A symbol without carriers, such as a label, stands
387
- * on its own.
388
- */
389
- const carriedOnlyOn = (features) => (symbol) => symbol.carriers.length > 0 && symbol.carriers.every(carrier => features.has(idOf(carrier)));
390
- /** The symbols of the versions that no other copy carries. */
391
- export const symbolsCarriedOnlyBy = (edition, copyId) => {
392
- const copy = edition.copies.find(c => c.id === copyId);
393
- return copy ? insertedIn(edition.versions).filter(carriedOnlyOn(featureIdsOf(copy))) : [];
394
- };
395
- const references = [...placementRelations, 'pairedWith'];
396
- const forgetPerforations = (perforation, dropped) => references
397
- .filter(relation => {
398
- const reference = perforation[relation];
399
- return reference && dropped.has(idOf(reference));
400
- })
401
- .forEach(relation => { delete perforation[relation]; });
402
- const forgetFeatures = (symbol, features, dropped) => {
403
- symbol.carriers = symbol.carriers.filter(carrier => !features.has(idOf(carrier)));
404
- if (isPerforation(symbol))
405
- forgetPerforations(symbol, dropped);
406
- };
407
- const forgetCopy = (edit, features, dropped) => {
408
- if (edit.insert) {
409
- edit.insert = edit.insert.filter(symbol => !dropped.has(symbol.id));
410
- edit.insert.forEach(symbol => forgetFeatures(symbol, features, dropped));
411
- }
412
- if (edit.delete) {
413
- edit.delete = edit.delete.filter(id => !dropped.has(id));
414
- }
415
- };
416
- const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
417
- /** Drops the edits the removal has emptied and leaves those that were empty before alone. */
418
- const forgetCopyIn = (version, features, dropped) => {
419
- const emptyBefore = new Set(version.edits.filter(isEmpty).map(edit => edit.id));
420
- version.edits.forEach(edit => forgetCopy(edit, features, dropped));
421
- version.edits = version.edits.filter(edit => !isEmpty(edit) || emptyBefore.has(edit.id));
422
- };
423
- /**
424
- * Takes the copy out of the edition together with the symbols only it
425
- * carries, and with every reference the versions made to those symbols.
426
- */
427
- export const removeCopy = (copyId) => draft => {
428
- const copy = draft.copies.find(c => c.id === copyId);
429
- if (!copy)
430
- return;
431
- const features = featureIdsOf(copy);
432
- const dropped = new Set(insertedIn(draft.versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
433
- draft.copies = draft.copies.filter(c => c.id !== copyId);
434
- draft.versions.forEach(version => forgetCopyIn(version, features, dropped));
435
- };
436
- export class MergeEdits extends BasePlan {
437
- constructor(versionId, toMerge) {
438
- super();
439
- Object.defineProperty(this, "versionId", {
440
- enumerable: true,
441
- configurable: true,
442
- writable: true,
443
- value: versionId
444
- });
445
- Object.defineProperty(this, "toMerge", {
446
- enumerable: true,
447
- configurable: true,
448
- writable: true,
449
- value: toMerge
450
- });
451
- }
452
- guessMotivation(edit) {
453
- const view = this.view;
454
- if (!view)
455
- return 'correct-error';
456
- const inserts = (edit.insert || []);
457
- const deletes = view.getAll(edit.delete ?? []);
458
- const types = [
459
- inserts.filter(e => e.type === 'expression').map(e => e.expressionType),
460
- deletes.filter(e => e.type === 'expression').map(e => e.expressionType)
461
- ];
462
- if (arraysIdentical(types, [['SlowCrescendoOn', 'SlowCrescendoOff'], []])) {
463
- return 'additional-accent';
464
- }
465
- else if (arraysIdentical(types, [['ForzandoOn', 'ForzandoOff'], []])) {
466
- // TODO: check if the inserts are very close
467
- // and return 'short-dynamic-differentation'
468
- return 'additional-accent';
469
- }
470
- else if (types.every(t => t.length > 1) && arraysIdentical(types[0], types[1])) {
471
- return 'shift';
472
- }
473
- else if (types[0].length === 0 && types[1].length === 1) {
474
- return 'remove-redundancy';
475
- }
476
- if (inserts.length === 1 && deletes.length === 1) {
477
- const insertDim = view.dimensionOf(inserts[0])?.horizontal;
478
- const deleteDim = view.dimensionOf(deletes[0])?.horizontal;
479
- if (!insertDim || !deleteDim) {
480
- return 'correct-error';
481
- }
482
- if (Math.abs(insertDim.from - deleteDim.from) < 5) {
483
- const insertLength = Math.abs(insertDim.to - insertDim.from);
484
- const deleteLength = Math.abs(deleteDim.to - deleteDim.from);
485
- if (insertLength < deleteLength) {
486
- return 'shorten';
487
- }
488
- else {
489
- return 'prolong';
490
- }
491
- }
492
- }
493
- return 'correct-error';
494
- }
495
- build() {
496
- const view = this.view;
497
- if (!view)
498
- return [];
499
- const result = structuredClone(this.toMerge[0]);
500
- this.toMerge
501
- .slice(1)
502
- .forEach(edit => {
503
- if (result.insert) {
504
- result.insert.push(...(edit.insert || []));
505
- }
506
- else {
507
- result.insert = edit.insert;
508
- }
509
- if (result.delete) {
510
- result.delete.push(...(edit.delete || []));
511
- }
512
- else {
513
- result.delete = edit.delete;
514
- }
515
- });
516
- const edit = {
517
- ...result,
518
- id: v4(),
519
- motivation: this.guessMotivation(result),
520
- };
521
- return ([draft => {
522
- const version = draft.versions.find(v => v.id === this.versionId);
523
- if (!version)
524
- return;
525
- // remove all edits that were merged
526
- for (const e of this.toMerge) {
527
- const index = version.edits.findIndex(ve => ve.id === e.id);
528
- if (index !== -1)
529
- version.edits.splice(index, 1);
530
- }
531
- version.edits.push(edit);
532
- }]);
533
- }
534
- }
535
- export class SplitEdit extends BasePlan {
536
- constructor(versionId, toSplit) {
537
- super();
538
- Object.defineProperty(this, "versionId", {
539
- enumerable: true,
540
- configurable: true,
541
- writable: true,
542
- value: versionId
543
- });
544
- Object.defineProperty(this, "toSplit", {
545
- enumerable: true,
546
- configurable: true,
547
- writable: true,
548
- value: toSplit
549
- });
550
- }
551
- build() {
552
- const view = this.view;
553
- if (!view)
554
- return [];
555
- const result = [];
556
- for (const insert of this.toSplit.insert ?? []) {
557
- result.push({
558
- type: 'edit',
559
- id: v4(),
560
- insert: [insert]
561
- });
562
- }
563
- for (const remove of this.toSplit.delete ?? []) {
564
- result.push({
565
- type: 'edit',
566
- id: v4(),
567
- delete: [remove]
568
- });
569
- }
570
- return ([draft => {
571
- const version = draft.versions.find(v => v.id === this.versionId);
572
- if (!version)
573
- return;
574
- // remove the edit that is split
575
- const index = version.edits.findIndex(ve => ve.id === this.toSplit.id);
576
- if (index !== -1)
577
- version.edits.splice(index, 1);
578
- version.edits.push(...result);
579
- }]);
580
- }
581
- }
582
- /*
583
- type LazyDimension = Partial<{ from: number, to: number }>;
584
-
585
- type LazyArea = {
586
- horizontal: LazyDimension;
587
- vertical: LazyDimension;
588
- }
589
-
590
- const overlaps = (a: LazyArea, b: LazyArea): boolean => {
591
- const overlapsDimension = (a: LazyDimension, b: LazyDimension): boolean =>
592
- (a.from ?? 0) < (b.to ?? Infinity) && (b.from ?? 0) < (a.to ?? Infinity);
593
-
594
- // console.log('do the dimensions overlap?', a, b)
595
-
596
- return overlapsDimension(a.horizontal, b.horizontal);
597
- }
598
- */
599
- const arraysIdentical = (a, b) => {
600
- let i = a.length;
601
- if (i != b.length)
602
- return false;
603
- while (i--) {
604
- if (Array.isArray(a[i]) && Array.isArray(b[i])) {
605
- return arraysIdentical(a[i], b[i]);
606
- }
607
- if (a[i] !== b[i])
608
- return false;
609
- }
610
- return true;
611
- };
@@ -1,12 +0,0 @@
1
- import { AnyFeature } from "./Feature";
2
- import { TrackerBar } from "./TrackerBar";
3
- type AlignmentResult = {
4
- shift: number;
5
- stretch: number;
6
- };
7
- /**
8
- * Align two rolls by computing independent linear fits of each roll's note-onset positions
9
- * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
10
- */
11
- export declare function alignFeatures(rollA: AnyFeature[], rollB: AnyFeature[], bar?: TrackerBar): AlignmentResult;
12
- export {};
@@ -1,55 +0,0 @@
1
- import { welteT100 } from "./TrackerBar";
2
- const isNoteOn = (bar) => (feature) => {
3
- return feature.type === 'Hole' && bar.meaningOf(feature.vertical.from)?.type === 'note';
4
- };
5
- /**
6
- * Fit a line: position = alpha * index + beta via least squares.
7
- */
8
- function fitIndexToPosition(indices, positions) {
9
- const n = indices.length;
10
- const meanIdx = indices.reduce((s, i) => s + i, 0) / n;
11
- const meanPos = positions.reduce((s, p) => s + p, 0) / n;
12
- let num = 0;
13
- let den = 0;
14
- for (let i = 0; i < n; i++) {
15
- const d = indices[i] - meanIdx;
16
- num += d * (positions[i] - meanPos);
17
- den += d * d;
18
- }
19
- const alpha = den === 0 ? 1 : num / den;
20
- const beta = meanPos - alpha * meanIdx;
21
- return { alpha, beta };
22
- }
23
- /**
24
- * Selects the first and last N elements of an array (or fewer if length is smaller).
25
- */
26
- function selectEnds(arr, count) {
27
- const n = arr.length;
28
- if (count * 2 >= n)
29
- return arr.slice();
30
- return arr.slice(0, count).concat(arr.slice(n - count, n));
31
- }
32
- /**
33
- * Align two rolls by computing independent linear fits of each roll's note-onset positions
34
- * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
35
- */
36
- export function alignFeatures(rollA, rollB, bar = welteT100) {
37
- // 1. Extract note-onset positions
38
- const isNote = isNoteOn(bar);
39
- const allXA = rollA.filter(isNote).map(f => f.horizontal.from);
40
- const allXB = rollB.filter(isNote).map(f => f.horizontal.from);
41
- // 2. Determine segment size (e.g. 10% of notes, min 5)
42
- const segCount = Math.max(5, Math.floor(allXA.length * 0.1));
43
- // 3. Select only first and last segments
44
- const XA = selectEnds(allXA, segCount);
45
- const idxA = XA.map((_, i) => i);
46
- const XB = selectEnds(allXB, segCount);
47
- const idxB = XB.map((_, i) => i);
48
- // 4. Fit index->position for each roll on selected ends
49
- const { alpha: alphaA, beta: betaA } = fitIndexToPosition(idxA, XA);
50
- const { alpha: alphaB, beta: betaB } = fitIndexToPosition(idxB, XB);
51
- // 5. Derive stretch and shift such that x2 = (x1 + shift) * stretch
52
- const stretch = alphaB / alphaA;
53
- const shift = betaB / stretch - betaA;
54
- return { stretch, shift };
55
- }
File without changes
File without changes
File without changes