linked-rolls 0.19.1 → 0.20.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.
@@ -1,27 +1,16 @@
1
1
  import { EditionView } from "./EditionView";
2
- import { TrackerBar } from "./TrackerBar";
3
2
  export type ConstraintProblem = {
4
3
  version: string;
5
4
  symbol: string;
6
- problem: 'alignment-reference-missing' | 'before-reference-missing' | 'after-reference-missing' | 'placed-relative-to-itself' | 'placed-several-ways' | 'partner-missing' | 'paired-with-itself' | 'in-several-pairs' | 'pair-placed-on-both-sides';
5
+ problem: 'alignment-reference-missing' | 'before-reference-missing' | 'after-reference-missing' | 'placed-relative-to-itself' | 'placed-several-ways' | 'partner-missing' | 'paired-with-itself' | 'in-several-pairs' | 'pair-placed-on-both-sides' | 'type-not-on-the-bar' | 'carrier-on-another-track' | 'copies-disagree-on-the-paper';
7
6
  };
8
7
  /**
9
- * Where the placements and pairings of the edition cannot hold as
10
- * stated, version by version: a reference or partner absent from the
11
- * version, a perforation placed relative to itself or in several ways
12
- * at once, one claimed by several pairs, or a pair whose members are
13
- * both placed and so cannot keep their distance and follow their
14
- * references at once.
8
+ * Where the edition cannot hold as stated, version by version: a
9
+ * placement or pairing reference absent from the version, a perforation
10
+ * placed relative to itself or in several ways at once, one claimed by
11
+ * several pairs, a pair whose members are both placed and so cannot
12
+ * keep their distance and follow their references at once, an
13
+ * expression the version's own bar cannot read, and a carrier sitting
14
+ * on a track that does not say what its symbol says.
15
15
  */
16
16
  export declare const constraintProblems: (view: EditionView) => ConstraintProblem[];
17
- export type UnknownExpressionType = {
18
- version: string;
19
- symbol: string;
20
- expressionType: string;
21
- };
22
- /**
23
- * The expressions of the edition whose type the tracker bar does not
24
- * read, version by version. The roll names its system; a type from
25
- * another system, or a misspelt one, means nothing on it.
26
- */
27
- export declare const unknownExpressionTypes: (view: EditionView, bar: TrackerBar) => UnknownExpressionType[];
@@ -1,5 +1,8 @@
1
1
  import { idOf } from "./Assumption";
2
2
  import { isPerforation, pairsAmong, placementsOf } from "./Symbol";
3
+ import { keyOf } from "./TrackerBar";
4
+ import { trackerBarOf } from "./systems";
5
+ import { barOf } from "./RollCopy";
3
6
  const missingReference = {
4
7
  alignedWith: 'alignment-reference-missing',
5
8
  before: 'before-reference-missing',
@@ -39,25 +42,78 @@ const problemsIn = (version, perforations) => {
39
42
  ...missingPartners, ...selfPaired, ...inSeveralPairs, ...placedOnBothSides
40
43
  ];
41
44
  };
42
- /**
43
- * Where the placements and pairings of the edition cannot hold as
44
- * stated, version by version: a reference or partner absent from the
45
- * version, a perforation placed relative to itself or in several ways
46
- * at once, one claimed by several pairs, or a pair whose members are
47
- * both placed and so cannot keep their distance and follow their
48
- * references at once.
49
- */
50
- export const constraintProblems = (view) => view.edition.versions.flatMap(version => problemsIn(version.id, view.snapshot(version.id).filter(isPerforation)));
51
45
  const isExpression = (symbol) => symbol.type === 'expression';
52
46
  /**
53
- * The expressions of the edition whose type the tracker bar does not
54
- * read, version by version. The roll names its system; a type from
55
- * another system, or a misspelt one, means nothing on it.
47
+ * The expressions the version's own bar cannot read, its type belonging
48
+ * to another system or misspelt.
49
+ *
50
+ * This is what says a transfer between systems is unfinished. A green
51
+ * version connected to a red one inherits every red expression, and
52
+ * none of them means anything on a green machine, so each is reported
53
+ * until an edit deletes it and says what took its place. The list
54
+ * emptying is the proof that the transfer is complete.
56
55
  */
57
- export const unknownExpressionTypes = (view, bar) => {
56
+ const typesNotOnTheBar = (version, snapshot) => {
57
+ const bar = trackerBarOf(version.system);
58
+ if (!bar)
59
+ return [];
58
60
  const known = new Set(bar.expressionTypes);
59
- return view.edition.versions.flatMap(version => view.snapshot(version.id)
61
+ return snapshot
60
62
  .filter(isExpression)
61
63
  .filter(symbol => !known.has(symbol.expressionType))
62
- .map(symbol => ({ version: version.id, symbol: symbol.id, expressionType: symbol.expressionType })));
64
+ .map(symbol => ({ version: version.id, symbol: symbol.id, problem: 'type-not-on-the-bar' }));
65
+ };
66
+ /**
67
+ * Carriers whose track does not say what the symbol they carry says.
68
+ *
69
+ * A carrier stands as evidence for its symbol, so the bar of the copy
70
+ * it sits on must read its track as the symbol's own meaning. Since a
71
+ * copy cut for another system numbers its tracks differently, this is
72
+ * what tells a transfer apart from a miscalibration: a red hole on 47
73
+ * and a green one on 45 both say pitch 60, while a green copy one track
74
+ * out says 59. Collation across systems consults only the place, so
75
+ * without this nothing checks the tracks at all.
76
+ */
77
+ const carriersOffTheirMeaning = (view, version, perforations) => {
78
+ const misread = (carrier, symbol) => {
79
+ const copy = view.copyOf(carrier.id);
80
+ if (!copy)
81
+ return false;
82
+ const meaning = barOf(copy).meaningOf(carrier.vertical.from);
83
+ return !meaning || keyOf(meaning) !== keyOf(symbol);
84
+ };
85
+ return perforations
86
+ .filter(symbol => view.carriersOf(symbol).some(carrier => misread(carrier, symbol)))
87
+ .map(symbol => ({ version, symbol: symbol.id, problem: 'carrier-on-another-track' }));
63
88
  };
89
+ /**
90
+ * Where the copies of the version's own system disagree about the scale
91
+ * that put them on the edition's shared axis.
92
+ *
93
+ * That scale is what takes a place back to the paper the version's roll
94
+ * ran on, so a performance needs one number. Copies disagreeing about it
95
+ * is evidence about the copies, and averaging it away would hide both
96
+ * the disagreement and the fact that the playback rests on a guess.
97
+ */
98
+ const paperDisagreed = (view, version) => view.speedScalesIn(version).length > 1
99
+ ? [{ version: version.id, symbol: version.id, problem: 'copies-disagree-on-the-paper' }]
100
+ : [];
101
+ /**
102
+ * Where the edition cannot hold as stated, version by version: a
103
+ * placement or pairing reference absent from the version, a perforation
104
+ * placed relative to itself or in several ways at once, one claimed by
105
+ * several pairs, a pair whose members are both placed and so cannot
106
+ * keep their distance and follow their references at once, an
107
+ * expression the version's own bar cannot read, and a carrier sitting
108
+ * on a track that does not say what its symbol says.
109
+ */
110
+ export const constraintProblems = (view) => view.edition.versions.flatMap(version => {
111
+ const snapshot = view.snapshot(version.id);
112
+ const perforations = snapshot.filter(isPerforation);
113
+ return [
114
+ ...problemsIn(version.id, perforations),
115
+ ...typesNotOnTheBar(version, snapshot),
116
+ ...carriersOffTheirMeaning(view, version.id, perforations),
117
+ ...paperDisagreed(view, version)
118
+ ];
119
+ });
package/lib/context.d.ts CHANGED
@@ -1,3 +1,10 @@
1
1
  export { default as jsonLdContext } from './spec/context.json';
2
- /** The context of the Welte-Mignon T-100, added to an edition of a T-100 roll. */
2
+ /** The context of the Welte-Mignon T-100, added to a version coded for it. */
3
3
  export { default as welteT100JsonLdContext } from './spec/welte-t100.context.json';
4
+ /**
5
+ * The context of the Welte-Mignon Licensee. Its expression terms are
6
+ * the T-100's, the two systems differing in the tracker scale rather
7
+ * than in the coding, so it reads them out of the T-100's vocabulary
8
+ * rather than minting a second set of IRIs for the same commands.
9
+ */
10
+ export { default as welteLicenseeJsonLdContext } from './spec/welte-licensee.context.json';
package/lib/context.js CHANGED
@@ -1,3 +1,10 @@
1
1
  export { default as jsonLdContext } from './spec/context.json';
2
- /** The context of the Welte-Mignon T-100, added to an edition of a T-100 roll. */
2
+ /** The context of the Welte-Mignon T-100, added to a version coded for it. */
3
3
  export { default as welteT100JsonLdContext } from './spec/welte-t100.context.json';
4
+ /**
5
+ * The context of the Welte-Mignon Licensee. Its expression terms are
6
+ * the T-100's, the two systems differing in the tracker scale rather
7
+ * than in the coding, so it reads them out of the T-100's vocabulary
8
+ * rather than minting a second set of IRIs for the same commands.
9
+ */
10
+ export { default as welteLicenseeJsonLdContext } from './spec/welte-licensee.context.json';
@@ -17,7 +17,9 @@ import { AnyFeature, FeatureConditionAssignment } from "./Feature";
17
17
  export type EditionOp = (draft: Draft<Edition>) => void;
18
18
  /**
19
19
  * Puts the copy into the edition with a version of its own, which
20
- * inserts every symbol the tracker bar reads on the copy.
20
+ * inserts every symbol the copy's own tracker bar reads on it. The
21
+ * version is a reading in that system's words, so it is coded for the
22
+ * system the copy was cut for.
21
23
  */
22
24
  export declare const createVersion: (siglum: string, copy: RollCopy) => EditionOp;
23
25
  /**
package/lib/editionOps.js CHANGED
@@ -4,7 +4,10 @@ import { getAt } from "./EditionView";
4
4
  import { isPerforation, placementRelations } from "./Symbol";
5
5
  import { collationsOf, defaultCollationTolerance } from "./Collation";
6
6
  import { collationToleranceOf, insertedBy } from "./Version";
7
- import { asSymbols } from "./RollCopy";
7
+ import { asSymbols, barOf, isPaperStretch } from "./RollCopy";
8
+ import { systemOf } from "./TrackerBar";
9
+ import { substitutionsBetween } from "./substitution";
10
+ import { trackerBarOf } from "./systems";
8
11
  import { applyShift, applyScale, revertShift, revertScale } from "./alignment";
9
12
  import { assignReference, idOf } from "./Assumption";
10
13
  import { conditions as conditionsAllowed, featuresBorneBy, isGluedOn, withBorneFeatures } from "./Feature";
@@ -66,20 +69,23 @@ const dropInsertions = (version, symbolIds) => {
66
69
  };
67
70
  /**
68
71
  * Puts the copy into the edition with a version of its own, which
69
- * inserts every symbol the tracker bar reads on the copy.
72
+ * inserts every symbol the copy's own tracker bar reads on it. The
73
+ * version is a reading in that system's words, so it is coded for the
74
+ * system the copy was cut for.
70
75
  */
71
76
  export const createVersion = (siglum, copy) => draft => {
77
+ const bar = barOf(copy);
72
78
  draft.copies.push(copy);
73
79
  draft.versions.push({
74
80
  type: 'Version',
75
81
  id: v4(),
76
82
  siglum,
83
+ system: systemOf(bar),
77
84
  versionType: 'edition',
78
- edits: asSymbols(copy.features).map(insertion),
85
+ edits: asSymbols(copy.features, bar).map(insertion),
79
86
  motivations: []
80
87
  });
81
88
  };
82
- const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
83
89
  /** States what the scale is put down to, in place of an earlier reading. */
84
90
  const readScale = (copy, reading) => {
85
91
  if (reading.cause === 'paper') {
@@ -451,6 +457,12 @@ const handOverCarriers = (view, draft, collations) => collations.forEach(({ symb
451
457
  const target = path && getAt(path, draft);
452
458
  target?.carriers.push(...symbol.carriers);
453
459
  });
460
+ /** Whether the two versions are coded for different reproducing systems. */
461
+ const differ = (child, parent) => {
462
+ const one = trackerBarOf(child?.system);
463
+ const other = trackerBarOf(parent?.system);
464
+ return one !== undefined && other !== undefined && one.id !== other.id;
465
+ };
454
466
  /**
455
467
  * Bases the child on the parent. A symbol of the child that collates
456
468
  * with one the parent hands down adds its carriers to that symbol; the
@@ -461,12 +473,33 @@ const handOverCarriers = (view, draft, collations) => collations.forEach(({ symb
461
473
  export const connectVersions = (view, childId, parentId, tolerance = defaultCollationTolerance) => {
462
474
  const inherited = view.snapshot(parentId);
463
475
  const own = view.snapshot(childId);
464
- const collations = collationsOf(own, inherited, symbol => view.dimensionOf(symbol), tolerance);
476
+ const locate = (symbol) => view.placeOf(symbol);
477
+ const collations = collationsOf(own, inherited, locate, tolerance);
465
478
  const collated = new Set(collations.map(({ symbol }) => symbol.id));
466
479
  const matched = new Set(collations.map(({ counterpart }) => counterpart.id));
480
+ /**
481
+ * Where the child is coded for another system, a held perforation of
482
+ * its own often stands for a latched pair of the parent's. Saying so
483
+ * as one edit is the transfer being carried out, and leaving the two
484
+ * apart would make the apparatus a list of unexplained losses beside
485
+ * a list of unexplained gains.
486
+ */
487
+ const substituted = differ(view.get(childId), view.get(parentId))
488
+ ? substitutionsBetween(own.filter(symbol => !collated.has(symbol.id)), inherited.filter(symbol => !matched.has(symbol.id)), locate, tolerance)
489
+ : [];
490
+ const paired = new Set(substituted.flatMap(({ replaced, by }) => [...by, ...replaced].map(symbol => symbol.id)));
467
491
  const edits = [
468
- ...own.filter(symbol => !collated.has(symbol.id)).map(insertion),
469
- ...inherited.filter(symbol => !matched.has(symbol.id)).map(symbol => deletion(symbol.id))
492
+ ...substituted.map(({ replaced, by }) => ({
493
+ type: 'edit',
494
+ id: v4(),
495
+ editType: 'replace-with-equivalent',
496
+ insert: [...by],
497
+ delete: replaced.map(symbol => symbol.id)
498
+ })),
499
+ ...own.filter(symbol => !collated.has(symbol.id) && !paired.has(symbol.id)).map(insertion),
500
+ ...inherited
501
+ .filter(symbol => !matched.has(symbol.id) && !paired.has(symbol.id))
502
+ .map(symbol => deletion(symbol.id))
470
503
  ];
471
504
  return onVersion(childId, (child, draft) => {
472
505
  handOverCarriers(view, draft, collations);
@@ -485,7 +518,7 @@ export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
485
518
  return noChange;
486
519
  const chosen = new Set(symbolIds);
487
520
  const own = insertedIn([version]).filter(symbol => chosen.has(symbol.id));
488
- const collations = collationsOf(own, view.snapshot(idOf(version.basedOn)), symbol => view.dimensionOf(symbol), tolerance ?? collationToleranceOf(version.basedOn));
521
+ const collations = collationsOf(own, view.snapshot(idOf(version.basedOn)), symbol => view.placeOf(symbol), tolerance ?? collationToleranceOf(version.basedOn));
489
522
  const collated = new Set(collations.map(({ symbol }) => symbol.id));
490
523
  return onVersion(versionId, (version, draft) => {
491
524
  handOverCarriers(view, draft, collations);
@@ -526,6 +559,7 @@ export const deriveVersion = (versionId, editIds) => onVersion(versionId, (versi
526
559
  type: 'Version',
527
560
  id: v4(),
528
561
  siglum: `${version.siglum}_derived`,
562
+ system: stateOf(version).system,
529
563
  versionType: 'unicum',
530
564
  basedOn: assignReference(versionId),
531
565
  edits: moved,
@@ -534,25 +568,53 @@ export const deriveVersion = (versionId, editIds) => onVersion(versionId, (versi
534
568
  });
535
569
  const sameSequence = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
536
570
  const expressionTypesOf = (symbols) => symbols.filter((symbol) => symbol.type === 'expression').map(symbol => symbol.expressionType);
537
- const accents = [['SlowCrescendoOn', 'SlowCrescendoOff'], ['ForzandoOn', 'ForzandoOff']];
571
+ /**
572
+ * How a single added accent is spelled. The red Welte latches a valve
573
+ * on and off again, the green holds one perforation for as long as the
574
+ * accent lasts, so the spelling is the system's and not the music's.
575
+ * Which of them a bar can spell decides which ones it is offered.
576
+ */
577
+ const ACCENTS = [
578
+ ['SlowCrescendoOn', 'SlowCrescendoOff'],
579
+ ['ForzandoOn', 'ForzandoOff'],
580
+ ['Crescendo'],
581
+ ['SforzandoForte']
582
+ ];
583
+ const accentsOn = (bar) => bar ? ACCENTS.filter(accent => accent.every(type => bar.expressionTypes.includes(type))) : [];
538
584
  const lengthOf = (span) => subtract(span.to, span.from);
539
585
  /** How far apart two onsets may lie for the one symbol to count as a replacement of the other. */
540
586
  const REPLACEMENT_TOLERANCE = mm(5);
541
587
  /** Shorten or prolong, where the inserted symbol starts about where the deleted one did. */
542
588
  const replacementType = (view, inserted, deleted) => {
543
- const after = view.dimensionOf(inserted)?.horizontal;
544
- const before = view.dimensionOf(deleted)?.horizontal;
589
+ const after = view.placeOf(inserted);
590
+ const before = view.placeOf(deleted);
545
591
  if (!after || !before || distance(after.from, before.from) >= REPLACEMENT_TOLERANCE)
546
592
  return undefined;
547
593
  return lengthOf(after) < lengthOf(before) ? 'shorten' : 'prolong';
548
594
  };
549
- /** A guess at what an edit does, from the symbols it exchanges. */
550
- const guessEditType = (view, edit) => {
595
+ /**
596
+ * A guess at what an edit does, from the symbols it exchanges and from
597
+ * the systems the version and the one it is based on are coded for.
598
+ */
599
+ const guessEditType = (view, versionId, edit) => {
551
600
  const inserts = edit.insert ?? [];
552
601
  const deletes = view.getAll(edit.delete ?? []);
553
602
  const inserted = expressionTypesOf(inserts);
554
603
  const deleted = expressionTypesOf(deletes);
555
- if (deleted.length === 0 && accents.some(accent => sameSequence(inserted, accent)))
604
+ const bar = trackerBarOf(view.get(versionId)?.system);
605
+ const parentBar = trackerBarOf(view.predecessorOf(versionId)?.system);
606
+ /**
607
+ * Where the version is coded for another system than its parent, an
608
+ * exchange of expression matter is the transfer being carried out:
609
+ * a red ForzandoOn and ForzandoOff pair giving way to one held green
610
+ * SforzandoForte says the same thing in the other system's words,
611
+ * which is what 'replace-with-equivalent' is for. Calling it a
612
+ * corrected error would say the editor made a mistake.
613
+ */
614
+ if (bar && parentBar && bar.id !== parentBar.id && inserted.length > 0 && deleted.length > 0) {
615
+ return 'replace-with-equivalent';
616
+ }
617
+ if (deleted.length === 0 && accentsOn(bar).some(accent => sameSequence(inserted, accent)))
556
618
  return 'additional-accent';
557
619
  if (inserted.length > 1 && sameSequence(inserted, deleted))
558
620
  return 'shift';
@@ -575,7 +637,7 @@ export const mergeEdits = (view, versionId, toMerge) => {
575
637
  insert: toMerge.flatMap(edit => edit.insert ?? []),
576
638
  delete: toMerge.flatMap(edit => edit.delete ?? [])
577
639
  };
578
- merged.editType = guessEditType(view, merged);
640
+ merged.editType = guessEditType(view, versionId, merged);
579
641
  const mergedIds = new Set(toMerge.map(edit => edit.id));
580
642
  return onVersion(versionId, version => {
581
643
  version.edits = [...version.edits.filter(edit => !mergedIds.has(edit.id)), merged];
@@ -23,10 +23,12 @@ const fromJsonLdValue = (value) => {
23
23
  /**
24
24
  * An entity with its keywords read as plain keys. The input is left as
25
25
  * it is. The `@type` of a value object names the datatype of the value,
26
- * not a class, and is dropped.
26
+ * not a class, and is dropped. So is a context: a version carries one
27
+ * naming its system's vocabulary, and it belongs to the serialisation
28
+ * rather than to the edition, which states the system as data.
27
29
  */
28
30
  const fromJsonLdEntity = (json) => {
29
- const { '@type': type, '@id': id, ...rest } = json;
31
+ const { '@type': type, '@id': id, '@context': context, ...rest } = json;
30
32
  const entity = Object.fromEntries(Object.entries(rest).map(([key, value]) => [key, fromJsonLdValue(value)]));
31
33
  if (type !== undefined && !('@value' in json))
32
34
  entity.type = type;
package/lib/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export * from './editionOps';
25
25
  export * from './Emulation';
26
26
  export * from './validate';
27
27
  export * from './constraints';
28
+ export * from './substitution';
28
29
  export * from './context';
29
30
  export * from './asJsonLd';
30
31
  export * from './importJsonLd';
package/lib/index.js CHANGED
@@ -25,6 +25,7 @@ export * from './editionOps';
25
25
  export * from './Emulation';
26
26
  export * from './validate';
27
27
  export * from './constraints';
28
+ export * from './substitution';
28
29
  export * from './context';
29
30
  export * from './asJsonLd';
30
31
  export * from './importJsonLd';
package/lib/migrate.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { conditions } from "./Feature";
2
2
  import { rollConditions } from "./RollCopy";
3
- import { systemOf } from "./TrackerBar";
3
+ import { systemIdIn, systemOf, translationBetween } from "./TrackerBar";
4
+ import { trackerBars } from "./systems";
4
5
  import { welteT100 } from "./systems/welteT100/bar";
5
6
  import { versionTypes } from "./Version";
6
7
  const versionTypeValues = new Set(versionTypes);
@@ -88,20 +89,93 @@ const walk = (value) => {
88
89
  return withWalkedChildren(migrateNode(value));
89
90
  return value;
90
91
  };
92
+ /** The production of a copy, under whichever of its two names it carries. */
93
+ const productionKeyOf = (copy) => copy?.productionEvent && !copy?.production ? 'productionEvent' : 'production';
94
+ /** The list under the key, or nothing where the document holds something else there. */
95
+ const listAt = (edition, key) => Array.isArray(edition[key]) ? edition[key] : undefined;
96
+ /** A file written while the system belonged to the roll rather than to each version. */
97
+ const namesSystemOnTheRoll = (edition) => edition.roll?.system !== undefined
98
+ || (listAt(edition, 'versions') ?? []).some((version) => !version?.system);
99
+ const barNamed = (system) => trackerBars.find(bar => bar.id === systemIdIn(system?.['@id']));
100
+ const withSystem = (node, system) => node.system && typeof node.system === 'object' ? node : { ...node, system };
101
+ /** A span on the other bar, or nothing where that bar does not read one of its ends. */
102
+ const spanOnBar = (span, at) => {
103
+ const from = at(span.from);
104
+ if (from === undefined)
105
+ return undefined;
106
+ if (span.to === undefined)
107
+ return { ...span, from };
108
+ const to = at(span.to);
109
+ return to === undefined ? undefined : { ...span, from, to };
110
+ };
111
+ /** The features on the copy's own bar, those it does not read left out, patches and all. */
112
+ const featuresOnBar = (features, at) => features.flatMap((feature) => {
113
+ const vertical = feature.vertical?.unit === 'track'
114
+ ? spanOnBar(feature.vertical, at)
115
+ : feature.vertical;
116
+ if (!vertical)
117
+ return [];
118
+ return [{
119
+ ...feature,
120
+ vertical,
121
+ ...(Array.isArray(feature.features) && { features: featuresOnBar(feature.features, at) })
122
+ }];
123
+ });
124
+ /**
125
+ * A copy cut for another system had its holes put onto the roll's bar
126
+ * as it was read, so they are numbered in the roll's system and not in
127
+ * its own. They go back onto the bar the copy names, which is the only
128
+ * numbering that means anything once each copy is read by its own bar.
129
+ * Between two Welte scales the difference is two tracks, which is a
130
+ * legal position a whole tone away rather than a visible error.
131
+ */
132
+ const onOwnBar = (copy, rollSystem) => {
133
+ if (!copy || typeof copy !== 'object')
134
+ return copy;
135
+ const key = productionKeyOf(copy);
136
+ const own = copy[key]?.system;
137
+ const from = barNamed(rollSystem);
138
+ const to = own && typeof own === 'object' ? barNamed(own) : undefined;
139
+ if (!from || !to || from.id === to.id || !Array.isArray(copy.features))
140
+ return copy;
141
+ return { ...copy, features: featuresOnBar(copy.features, translationBetween(from, to)) };
142
+ };
91
143
  /**
92
- * Every 0.1 edition was read with the T-100 tracker bar, so a roll
93
- * without a system is a T-100 roll. The text a copy's production
94
- * gave for the system is kept as the name.
144
+ * Systems belonged to the roll before they belonged to the versions
145
+ * and the copies. Every 0.1 edition was read with the T-100 bar, so a
146
+ * roll that named no system was a T-100 roll, and the text a copy's
147
+ * production gave for it is kept as the system's name.
95
148
  */
96
- const withRollSystem = (edition) => {
97
- if (!edition.roll || edition.roll.system)
149
+ const withSystems = (edition) => {
150
+ if (!edition.roll || !namesSystemOnTheRoll(edition))
98
151
  return edition;
99
- const stated = (edition.copies ?? [])
100
- .map((copy) => copy.production?.system ?? copy.productionEvent?.system)
152
+ const versions = listAt(edition, 'versions');
153
+ const copies = listAt(edition, 'copies');
154
+ const stated = (copies ?? [])
155
+ .map((copy) => copy?.[productionKeyOf(copy)]?.system)
101
156
  .find((system) => typeof system === 'string' && system !== '');
102
157
  const { id, ...concept } = systemOf(welteT100);
103
- const system = { '@id': id, ...concept, ...(stated && { name: stated }) };
104
- return { ...edition, roll: { ...edition.roll, system } };
158
+ const system = edition.roll.system ?? { '@id': id, ...concept, ...(stated && { name: stated }) };
159
+ const { system: _named, ...roll } = edition.roll;
160
+ const created = roll.recordingEvent?.created;
161
+ return {
162
+ ...edition,
163
+ roll: created
164
+ ? { ...roll, recordingEvent: { ...roll.recordingEvent, created: withSystem(created, system) } }
165
+ : roll,
166
+ ...(versions && {
167
+ versions: versions.map((version) => withSystem(version, system))
168
+ }),
169
+ ...(copies && {
170
+ copies: copies.map((copy) => {
171
+ if (!copy || typeof copy !== 'object')
172
+ return copy;
173
+ const key = productionKeyOf(copy);
174
+ const onOwn = onOwnBar(copy, system);
175
+ return { ...onOwn, [key]: withSystem(onOwn[key] ?? {}, system) };
176
+ })
177
+ })
178
+ };
105
179
  };
106
180
  /** An edition written before the editors were carried names none. */
107
181
  const withEditors = (edition) => !edition.creation || edition.creation.editors
@@ -126,5 +200,5 @@ const withDerivationTolerance = (edition) => {
126
200
  : version)
127
201
  };
128
202
  };
129
- const editionSteps = [withRollSystem, withEditors, withDerivationTolerance];
203
+ const editionSteps = [withSystems, withEditors, withDerivationTolerance];
130
204
  export const migrate = (edition) => walk(editionSteps.reduce((result, step) => step(result), edition));
@@ -37,8 +37,6 @@ export interface PhillipsErollOptions {
37
37
  * the roll he read.
38
38
  */
39
39
  system?: TrackerBar;
40
- /** The edition's bar, onto which the positions are put. */
41
- bar?: TrackerBar;
42
40
  /**
43
41
  * How far above its position an expression number sits. Defaults to
44
42
  * what his samples show for that bar.
@@ -54,9 +52,9 @@ export interface PhillipsErollOptions {
54
52
  }
55
53
  /**
56
54
  * Reads one of his e-roll files as a copy of the roll, in millimetres
57
- * of paper. A position the edition's bar does not read is left out, as
58
- * the bar would leave it.
55
+ * of paper, on the bar it was cut for and in that bar's numbering. A
56
+ * position the bar does not read is left out, as the bar would leave it.
59
57
  */
60
- export declare function readFromPhillipsEroll(buffer: ArrayBuffer, { system, bar, controlOffset, placeAt }?: PhillipsErollOptions): RollCopy;
58
+ export declare function readFromPhillipsEroll(buffer: ArrayBuffer, { system, controlOffset, placeAt }?: PhillipsErollOptions): RollCopy;
61
59
  /** The bars his files are known to be numbered by. */
62
60
  export declare const phillipsSystems: readonly TrackerBar[];
@@ -1,7 +1,7 @@
1
1
  import { read } from "midifile-ts";
2
2
  import { v4 } from "uuid";
3
3
  import { assignObject } from "../Assumption";
4
- import { systemOf, translationBetween } from "../TrackerBar";
4
+ import { systemOf } from "../TrackerBar";
5
5
  import { welteT100 } from "../systems/welteT100/bar";
6
6
  import { welteLicensee } from "../systems/welteLicensee/bar";
7
7
  import { inMetersPerMinute, mm, seconds, track } from "../Quantity";
@@ -116,21 +116,20 @@ const positionsByPitch = (bar) => new Map(Array.from({ length: bar.trackCount },
116
116
  }));
117
117
  /**
118
118
  * Reads one of his e-roll files as a copy of the roll, in millimetres
119
- * of paper. A position the edition's bar does not read is left out, as
120
- * the bar would leave it.
119
+ * of paper, on the bar it was cut for and in that bar's numbering. A
120
+ * position the bar does not read is left out, as the bar would leave it.
121
121
  */
122
- export function readFromPhillipsEroll(buffer, { system = welteT100, bar = welteT100, controlOffset, placeAt } = {}) {
122
+ export function readFromPhillipsEroll(buffer, { system = welteT100, controlOffset, placeAt } = {}) {
123
123
  const file = read(new Uint8Array(buffer));
124
124
  const timed = onOneClock(file.tracks);
125
125
  const elapsedAt = clockOf(temposIn(timed), file.header.ticksPerBeat);
126
126
  const place = placeAt ?? atStatedSpeed(system);
127
127
  const offset = controlOffsetOf(system, controlOffset);
128
128
  const notePositions = positionsByPitch(system);
129
- const onBar = translationBetween(system, bar);
130
129
  const positionOf = (number) => notePositions.get(number) ?? track(number - offset);
131
130
  const features = notesIn(timed).flatMap((note) => {
132
- const position = onBar(positionOf(note.pitch));
133
- if (position === undefined)
131
+ const position = positionOf(note.pitch);
132
+ if (!system.meaningOf(position))
134
133
  return [];
135
134
  return [{
136
135
  type: 'Hole',
@@ -16,14 +16,13 @@ export interface SpencerBarOptions {
16
16
  * the roll it was scanned from. His Welte files are Licensee rolls.
17
17
  */
18
18
  system?: TrackerBar;
19
- /** The edition's bar, onto which the positions are put. */
20
- bar?: TrackerBar;
21
19
  }
22
20
  /**
23
- * Reads the copy onto the edition's bar. A hole on a position the
24
- * edition's bar does not read is left out, as the bar would leave it.
21
+ * Reads the copy on the bar it was cut for, whose numbering it keeps.
22
+ * A hole on a position that bar does not read is left out, as the bar
23
+ * would leave it.
25
24
  */
26
- export declare function readFromSpencerBar(buffer: ArrayBuffer, { rowsPerInch, system, bar }?: SpencerBarOptions): RollCopy;
25
+ export declare function readFromSpencerBar(buffer: ArrayBuffer, { rowsPerInch, system }?: SpencerBarOptions): RollCopy;
27
26
  /**
28
27
  * The `.ann` file beside a `.bar` holds the player's settings for the
29
28
  * roll as lines of "/key: value": title, composer, pianist, roll
@@ -1,6 +1,5 @@
1
1
  import { v4 } from "uuid";
2
- import { systemOf, translationBetween } from "../TrackerBar";
3
- import { welteT100 } from "../systems/welteT100/bar";
2
+ import { systemOf } from "../TrackerBar";
4
3
  import { welteLicensee } from "../systems/welteLicensee/bar";
5
4
  import { feetPerMinute, inMillimeters, px, track } from "../Quantity";
6
5
  /**
@@ -80,20 +79,20 @@ const holesOf = (events) => {
80
79
  return holes.sort((a, b) => a.from - b.from);
81
80
  };
82
81
  /**
83
- * Reads the copy onto the edition's bar. A hole on a position the
84
- * edition's bar does not read is left out, as the bar would leave it.
82
+ * Reads the copy on the bar it was cut for, whose numbering it keeps.
83
+ * A hole on a position that bar does not read is left out, as the bar
84
+ * would leave it.
85
85
  */
86
- export function readFromSpencerBar(buffer, { rowsPerInch = SPENCER_ROWS_PER_INCH, system = welteLicensee, bar = welteT100 } = {}) {
86
+ export function readFromSpencerBar(buffer, { rowsPerInch = SPENCER_ROWS_PER_INCH, system = welteLicensee } = {}) {
87
87
  const bytes = new Uint8Array(buffer);
88
88
  if (byteAt(bytes, TEXT_AT) !== TEXT_TAG) {
89
89
  throw new Error('Not a Spencer .bar file: no text after the header');
90
90
  }
91
91
  const placeOf = (row) => inMillimeters(px(row), rowsPerInch);
92
- const onBar = translationBetween(system, bar);
93
92
  const features = holesOf(eventsIn(bytes, endOfText(bytes, TEXT_AT + 1)))
94
93
  .flatMap((hole) => {
95
- const position = onBar(track(hole.position));
96
- if (position === undefined)
94
+ const position = track(hole.position);
95
+ if (!system.meaningOf(position))
97
96
  return [];
98
97
  return [{
99
98
  type: 'Hole',
@@ -8,13 +8,10 @@ export interface StanfordAtonOptions {
8
8
  * perforation on the bar's rewind track.
9
9
  */
10
10
  trackShift?: Track;
11
- /** The edition's bar, onto which the holes are put. */
12
- bar?: TrackerBar;
13
11
  /**
14
- * The bar the scanned roll was cut for, where it is not the
15
- * edition's. The scan is calibrated on it, and the holes are then
16
- * put onto the edition's bar; one on a position that bar does not
17
- * read is left out.
12
+ * The bar the scanned roll was cut for. The scan is calibrated on
13
+ * it and the holes keep its numbering, since a copy is read by the
14
+ * bar it was cut for and by no other.
18
15
  */
19
16
  system?: TrackerBar;
20
17
  /**
@@ -24,4 +21,4 @@ export interface StanfordAtonOptions {
24
21
  */
25
22
  scan?: string;
26
23
  }
27
- export declare function readFromStanfordAton(atonString: string, { trackShift, bar, system, scan }?: StanfordAtonOptions): RollCopy;
24
+ export declare function readFromStanfordAton(atonString: string, { trackShift, system, scan }?: StanfordAtonOptions): RollCopy;