linked-rolls 0.0.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 (60) hide show
  1. package/README.md +18 -0
  2. package/lib/Collation.d.ts +4 -0
  3. package/lib/Collation.js +1 -0
  4. package/lib/Condition.d.ts +10 -0
  5. package/lib/Condition.js +1 -0
  6. package/lib/ConditionState.d.ts +9 -0
  7. package/lib/ConditionState.js +6 -0
  8. package/lib/Edit.d.ts +21 -0
  9. package/lib/Edit.js +112 -0
  10. package/lib/Edition.d.ts +54 -0
  11. package/lib/Edition.js +1 -0
  12. package/lib/EditorialAssumption.d.ts +67 -0
  13. package/lib/EditorialAssumption.js +26 -0
  14. package/lib/Emulation.d.ts +76 -0
  15. package/lib/Emulation.js +474 -0
  16. package/lib/Feature.d.ts +37 -0
  17. package/lib/Feature.js +3 -0
  18. package/lib/Measurement.d.ts +9 -0
  19. package/lib/Measurement.js +1 -0
  20. package/lib/PlaceTimeConversion.d.ts +65 -0
  21. package/lib/PlaceTimeConversion.js +175 -0
  22. package/lib/RollCopy.d.ts +85 -0
  23. package/lib/RollCopy.js +273 -0
  24. package/lib/RollEvent.d.ts +76 -0
  25. package/lib/RollEvent.js +3 -0
  26. package/lib/Stage.d.ts +37 -0
  27. package/lib/Stage.js +165 -0
  28. package/lib/Symbol.d.ts +62 -0
  29. package/lib/Symbol.js +29 -0
  30. package/lib/TrackerBar.d.ts +8 -0
  31. package/lib/TrackerBar.js +53 -0
  32. package/lib/Transcription.d.ts +7 -0
  33. package/lib/Transcription.js +9 -0
  34. package/lib/Version.d.ts +42 -0
  35. package/lib/Version.js +217 -0
  36. package/lib/WithId.d.ts +3 -0
  37. package/lib/WithId.js +1 -0
  38. package/lib/alignFeatures.d.ts +11 -0
  39. package/lib/alignFeatures.js +54 -0
  40. package/lib/alignRolls.d.ts +7 -0
  41. package/lib/alignRolls.js +49 -0
  42. package/lib/alignSymbols.d.ts +7 -0
  43. package/lib/alignSymbols.js +49 -0
  44. package/lib/asJsonLd.d.ts +3 -0
  45. package/lib/asJsonLd.js +67 -0
  46. package/lib/asMIDISpans.d.ts +23 -0
  47. package/lib/asMIDISpans.js +111 -0
  48. package/lib/aton/AtonParser.d.ts +48 -0
  49. package/lib/aton/AtonParser.js +245 -0
  50. package/lib/aton/AtonParser.test.d.ts +1 -0
  51. package/lib/aton/AtonParser.test.js +16 -0
  52. package/lib/build-schema.cjs +113 -0
  53. package/lib/context.d.ts +1 -0
  54. package/lib/context.js +1 -0
  55. package/lib/importJsonLd.d.ts +4 -0
  56. package/lib/importJsonLd.js +121 -0
  57. package/lib/index.d.ts +17 -0
  58. package/lib/index.js +17 -0
  59. package/lib/spec/context.json +192 -0
  60. package/package.json +27 -0
package/lib/Stage.js ADDED
@@ -0,0 +1,165 @@
1
+ import { v4 } from "uuid";
2
+ import { flat } from "./EditorialAssumption";
3
+ import { dimensionOf } from "./Symbol";
4
+ const versionType = [
5
+ /**
6
+ * The roll is in a state where it is used as
7
+ * the master roll for several new reproductions.
8
+ */
9
+ 'edition',
10
+ /**
11
+ * This denotes a stage which is specific to (early)
12
+ * Welte-Mignon piano rolls, where rolls inteded to
13
+ * be pulished are revised by a controller first. These
14
+ * rolls typically carry a "controlliert" stamp. The
15
+ * revision is done on the same date as the perforation
16
+ * and the date is written on the roll towards its end.
17
+ */
18
+ 'authorised-revision',
19
+ /**
20
+ * Unauthorised revisions are those, which cannot be linked
21
+ * to a specific controller and are likely done by
22
+ * a later, anonymous hand.
23
+ */
24
+ 'unauthorised-revision',
25
+ /**
26
+ * In the case of Welte Mignon rolls, glosses are
27
+ * typically comments about the roll's condition, added
28
+ * e.g. by the collector.
29
+ */
30
+ 'gloss'
31
+ ];
32
+ export const traverseStages = (stage, callback) => {
33
+ callback(stage);
34
+ if (stage.basedOn) {
35
+ traverseStages(flat(stage.basedOn), callback);
36
+ }
37
+ };
38
+ export const getSnapshot = (stage) => {
39
+ const snapshot = [];
40
+ const toDelete = [];
41
+ traverseStages(stage, s => {
42
+ snapshot.push(...s.edits.flatMap(edit => edit.insert || []));
43
+ // as we travel further up, remove symbols that are
44
+ // deleted in the stages further down
45
+ const deleted = [];
46
+ for (const toRemove of toDelete) {
47
+ const index = snapshot.findIndex(s => s === toRemove);
48
+ if (index !== -1) {
49
+ snapshot.splice(index, 1);
50
+ deleted.push(toRemove);
51
+ }
52
+ }
53
+ for (const del of deleted) {
54
+ toDelete.splice(toDelete.indexOf(del), 1);
55
+ }
56
+ // collect symbols that are deleted in the current stage
57
+ toDelete.push(...s.edits.flatMap(edit => edit.delete || []));
58
+ });
59
+ return snapshot.sort((a, b) => {
60
+ const aDimension = dimensionOf(a);
61
+ const bDimension = dimensionOf(b);
62
+ return aDimension.horizontal.from - bDimension.horizontal.from;
63
+ });
64
+ };
65
+ const isCollatable = (symbolA, symbolB) => {
66
+ // two symbols are collatible if they share the same
67
+ // symbol characteristics (pitch, expression type etc.)
68
+ // and occur in the same horizontal position.
69
+ if (symbolA.type === 'note' && symbolB.type === 'note') {
70
+ if (symbolA.pitch !== symbolB.pitch)
71
+ return false;
72
+ }
73
+ else if (symbolA.type === 'expression' && symbolB.type === 'expression') {
74
+ if (symbolA.expressionType !== symbolB.expressionType)
75
+ return false;
76
+ if (symbolA.scope !== symbolB.scope)
77
+ return false;
78
+ }
79
+ const dimensionA = dimensionOf(symbolA);
80
+ const dimensionB = dimensionOf(symbolB);
81
+ const distanceStart = Math.abs(dimensionA.horizontal.from - dimensionB.horizontal.from);
82
+ const distanceEnd = Math.abs(dimensionA.horizontal.to - dimensionB.horizontal.to);
83
+ if (distanceStart > 5 || distanceEnd > 5) {
84
+ // the symbols are too far apart to be collated
85
+ return false;
86
+ }
87
+ return true;
88
+ };
89
+ const overlaps = (a, b) => {
90
+ const overlapsDimension = (a, b) => (a.from ?? 0) < (b.to ?? Infinity) && (b.from ?? 0) < (a.to ?? Infinity);
91
+ return overlapsDimension(a.horizontal, b.horizontal) && overlapsDimension(a.vertical, b.vertical);
92
+ };
93
+ /**
94
+ * Walks through the stages. If it finds a symbol that is still
95
+ * part of the tradition (i.e. it is included in the current snapshot)
96
+ * and is equivalent with the given symbol, it will add the feature
97
+ * carrying the symbol to the collated symbol. Otherwise, the
98
+ * given symbol will be added to the current stage's insertions.
99
+ *
100
+ * All symbols of the tradition that are not included in the given
101
+ * symbols are considered to be deleted.
102
+ *
103
+ * @param creation
104
+ * @param symbols
105
+ * @returns
106
+ */
107
+ export function fillEdits(currentStage, symbols) {
108
+ const snapshot = getSnapshot(currentStage);
109
+ const treatedSymbols = [];
110
+ // can it be collated with any of the symbols of
111
+ // included in the current snapshot?
112
+ const insertions = [...symbols];
113
+ for (const symbol of symbols) {
114
+ snapshot
115
+ .filter(toCompare => isCollatable(symbol, toCompare))
116
+ .forEach(corresp => {
117
+ corresp.carriers.push(...symbol.carriers);
118
+ insertions.splice(insertions.indexOf(symbol), 1);
119
+ treatedSymbols.push(corresp);
120
+ });
121
+ }
122
+ // special treatment for covers
123
+ const covers = insertions.filter(symbol => symbol.type === 'cover');
124
+ for (const cover of covers) {
125
+ // find perforations in the snapshot that
126
+ // overlap with the cover
127
+ snapshot
128
+ .filter(symbol => symbol.type === 'note' || symbol.type === 'expression')
129
+ .map(dimensionOf)
130
+ .filter(dimension => overlaps(dimension, dimensionOf(cover)))
131
+ .forEach(dimension => {
132
+ const coverDimension = dimensionOf(cover);
133
+ // check if the cover partially covers the beginning
134
+ if (dimension.horizontal.from >= coverDimension.horizontal.from &&
135
+ dimension.horizontal.from <= coverDimension.horizontal.to) {
136
+ // the note starts where the cover ends
137
+ dimension.horizontal.from = coverDimension.horizontal.to;
138
+ }
139
+ // check if the cover partially covers the ending
140
+ if (dimension.horizontal.to >= coverDimension.horizontal.from &&
141
+ dimension.horizontal.to <= coverDimension.horizontal.to) {
142
+ // the note ends where the cover starts
143
+ dimension.horizontal.to = coverDimension.horizontal.from;
144
+ }
145
+ });
146
+ insertions.splice(insertions.indexOf(cover), 1);
147
+ }
148
+ currentStage.edits.push(...insertions.map((symbol) => {
149
+ return {
150
+ insert: [symbol],
151
+ delete: [],
152
+ id: v4(),
153
+ };
154
+ }));
155
+ const deletions = snapshot.filter(sym => {
156
+ return !treatedSymbols.includes(sym);
157
+ });
158
+ for (const symbol of deletions) {
159
+ currentStage.edits.push({
160
+ insert: [],
161
+ delete: [symbol],
162
+ id: v4(),
163
+ });
164
+ }
165
+ }
@@ -0,0 +1,62 @@
1
+ import { EditorialAssumption } from "./EditorialAssumption";
2
+ import { HorizontalSpan, RollFeature, VerticalSpan } from "./Feature";
3
+ import { WithId } from "./WithId";
4
+ /**
5
+ * @todo Should this be called Transcription?
6
+ */
7
+ export type CarrierAssignment = EditorialAssumption<'carrierAssignment', RollFeature>;
8
+ export interface Symbol<T extends string> extends WithId {
9
+ type: T;
10
+ carriers: CarrierAssignment[];
11
+ }
12
+ export declare const isSymbol: (object: any) => object is AnySymbol;
13
+ export interface Perforation<T extends string> extends Symbol<T> {
14
+ accelerating?: boolean;
15
+ }
16
+ export interface Note extends Perforation<'note'> {
17
+ pitch: number;
18
+ }
19
+ export type ExpressionScope = 'bass' | 'treble';
20
+ export type ExpressionType = 'SustainPedalOn' | 'SustainPedalOff' | 'SoftPedalOn' | 'SoftPedalOff' | 'MezzoforteOff' | 'MezzoforteOn' | 'SlowCrescendoOn' | 'SlowCrescendoOff' | 'ForzandoOn' | 'ForzandoOff' | 'MotorOff' | 'MotorOn' | 'Rewind' | 'ElectricCutOff';
21
+ export interface Expression extends Perforation<'expression'> {
22
+ scope: ExpressionScope;
23
+ expressionType: ExpressionType;
24
+ }
25
+ /**
26
+ * This denotes perforations that are covered by an editor.
27
+ * The covered perforation is not considered to be part
28
+ * of the original note or expression hole anymore.
29
+ */
30
+ export interface Cover extends Symbol<'cover'> {
31
+ /**
32
+ * This property can be used to indicate e.g. the
33
+ * color or material of the cover.
34
+ */
35
+ note?: string;
36
+ }
37
+ export interface Text<T extends string> extends Symbol<T> {
38
+ text: string;
39
+ rotation?: number;
40
+ }
41
+ /**
42
+ * For handwritten insertions like e. g. the
43
+ * perforation date in the end of a roll.
44
+ */
45
+ export interface HandwrittenText extends Text<'handwrittenText'> {
46
+ pen?: 'ink' | 'pencil' | 'crayon';
47
+ }
48
+ /**
49
+ * This type can be used to indicate stamps like e. g. the
50
+ * "controlliert" stamp in the beginning of rolls or the
51
+ * date at the end of (later) Welte rolls.
52
+ */
53
+ export interface Stamp extends Text<'stamp'> {
54
+ }
55
+ export interface RollLabel extends Text<'rollLabel'> {
56
+ signed: boolean;
57
+ }
58
+ export type AnySymbol = Note | Expression | HandwrittenText | Stamp | Cover | RollLabel;
59
+ export declare const dimensionOf: (symbol: AnySymbol) => {
60
+ horizontal: HorizontalSpan;
61
+ vertical: VerticalSpan;
62
+ };
package/lib/Symbol.js ADDED
@@ -0,0 +1,29 @@
1
+ import { flat } from "./EditorialAssumption";
2
+ export const isSymbol = (object) => {
3
+ return 'carriers' in object;
4
+ };
5
+ export const dimensionOf = (symbol) => {
6
+ if (symbol.carriers.length === 0) {
7
+ return {
8
+ horizontal: { unit: 'mm', from: 0, to: 0 },
9
+ vertical: { unit: 'track', from: 0, to: 0 }
10
+ };
11
+ }
12
+ const horizontalFrom = flat(symbol.carriers)
13
+ .reduce((hAcc, h) => hAcc + h.horizontal.from, 0) / symbol.carriers.length;
14
+ const horizontalTo = flat(symbol.carriers)
15
+ .reduce((hAcc, h) => hAcc + h.horizontal.to, 0) / symbol.carriers.length;
16
+ const verticalFrom = flat(symbol.carriers)
17
+ .reduce((vAcc, v) => vAcc + v.vertical.from, 0) / symbol.carriers.length;
18
+ const definedVerticalTo = flat(symbol.carriers)
19
+ .filter(v => v.vertical.to !== undefined);
20
+ let verticalTo = undefined;
21
+ if (definedVerticalTo.length > 0) {
22
+ verticalTo = definedVerticalTo
23
+ .reduce((vAcc, v) => vAcc + v.vertical.to, 0) / definedVerticalTo.length;
24
+ }
25
+ return {
26
+ horizontal: { unit: 'mm', from: horizontalFrom, to: horizontalTo },
27
+ vertical: { unit: 'track', from: verticalFrom, to: verticalTo }
28
+ };
29
+ };
@@ -0,0 +1,8 @@
1
+ import { Expression, Note } from "./Symbol";
2
+ export declare abstract class TrackerBar {
3
+ abstract meaningOf(track: number): Partial<Note> | Partial<Expression>;
4
+ }
5
+ export declare class WelteT100 {
6
+ meaningOf(track: number): Pick<Note, 'type' | 'pitch'> | Pick<Expression, 'type' | 'expressionType' | 'scope'>;
7
+ width: number;
8
+ }
@@ -0,0 +1,53 @@
1
+ export class TrackerBar {
2
+ }
3
+ export class WelteT100 {
4
+ constructor() {
5
+ Object.defineProperty(this, "width", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: 328
10
+ }); // in mm, cf. Hagmann, p. 75
11
+ }
12
+ meaningOf(track) {
13
+ if (track <= 0 || track > 100) {
14
+ throw new Error('Track out of range');
15
+ }
16
+ // Cf. Hagmann, p. 178
17
+ const expressionMap = new Map([
18
+ [1, 'MezzoforteOff'],
19
+ [2, 'MezzoforteOn'],
20
+ [3, 'SlowCrescendoOff'],
21
+ [4, 'SlowCrescendoOn'],
22
+ [5, 'ForzandoOff'],
23
+ [6, 'ForzandoOn'],
24
+ [7, 'SoftPedalOff'],
25
+ [8, 'SoftPedalOn'],
26
+ [9, 'MotorOff'],
27
+ [10, 'MotorOn'],
28
+ [91, 'Rewind'],
29
+ [92, 'ElectricCutOff'],
30
+ [93, 'SustainPedalOn'],
31
+ [94, 'SustainPedalOff'],
32
+ [95, 'ForzandoOn'],
33
+ [96, 'ForzandoOff'],
34
+ [97, 'SlowCrescendoOn'],
35
+ [98, 'SlowCrescendoOff'],
36
+ [99, 'MezzoforteOn'],
37
+ [100, 'MezzoforteOff']
38
+ ]);
39
+ if (track <= 10 || track >= 91) {
40
+ const scope = track <= 10 ? 'bass' : 'treble';
41
+ return {
42
+ expressionType: expressionMap.get(track),
43
+ scope,
44
+ type: 'expression'
45
+ };
46
+ }
47
+ // C1-G4 = track 11-90 = 24-103 in MIDI keys
48
+ return {
49
+ pitch: track + 13,
50
+ type: 'note',
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,7 @@
1
+ import { Observation } from "./EditorialAssumption";
2
+ /**
3
+ * Every transcription is an I1 Argumentation.
4
+ */
5
+ export interface Transcription extends Observation {
6
+ software?: string;
7
+ }
@@ -0,0 +1,9 @@
1
+ export {};
2
+ // Example:
3
+ // Edit -> carried out by -> "Anonym"
4
+ // Edit -> concluded by -> Observation: der äußeren Erscheinung nach eine spätere Hinzufügung von unbekanntere Hand
5
+ // Edit -> insert -> Symbol
6
+ // Symbol -> is carried by -> Roll Feature -> condition -> damaged
7
+ // Symbol -> concludedBy -> Transcription -> Die Perforationen wurden nicht präzise an die richtige Stelle gesetzt.
8
+ // Wir gehen davon aus, dass sie jeweils auf der darüberliegenden Linie
9
+ // platziert werden sollten.
@@ -0,0 +1,42 @@
1
+ import { ActorAssignment, Edit } from "./Edit";
2
+ import { EditorialAssumption, Motivation } from "./EditorialAssumption";
3
+ import { AnySymbol } from "./Symbol";
4
+ import { CollationTolerance } from "./Collation";
5
+ export type Derivation = EditorialAssumption<'derivation', Version>;
6
+ export declare const versionTypes: readonly ["edition", "authorised-revision", "unauthorised-revision", "gloss"];
7
+ export type VersionType = typeof versionTypes[number];
8
+ /**
9
+ * Version + Version Creation
10
+ */
11
+ export interface Version {
12
+ id: string;
13
+ siglum: string;
14
+ actor?: ActorAssignment;
15
+ basedOn?: Derivation;
16
+ edits: Edit[];
17
+ motivations: Motivation<string>[];
18
+ type: VersionType;
19
+ }
20
+ export declare const traverseVersions: (version: Version, callback: (version: Version) => void) => void;
21
+ export declare const getSnapshot: (version: Version) => AnySymbol[];
22
+ /**
23
+ * Walks through the versions. If it finds a symbol that is still
24
+ * part of the tradition (i.e. it is included in the current snapshot)
25
+ * and is equivalent with the given symbol, it will add the feature
26
+ * carrying the symbol to the collated symbol. Otherwise, the
27
+ * given symbol will be added to the current version's insertions.
28
+ *
29
+ * All symbols of the tradition that are not included in the given
30
+ * symbols are considered to be deleted.
31
+ *
32
+ * @param creation
33
+ * @param symbols
34
+ * @returns
35
+ */
36
+ export declare function fillEdits(currentVersion: Version, symbols: AnySymbol[], tolerance?: CollationTolerance): void;
37
+ /**
38
+ * Assigns a generation (depth) to every node.
39
+ */
40
+ export declare function assignGenerations(versions: Version[]): Array<Version & {
41
+ generation: number;
42
+ }>;
package/lib/Version.js ADDED
@@ -0,0 +1,217 @@
1
+ import { v4 } from "uuid";
2
+ import { flat } from "./EditorialAssumption";
3
+ import { dimensionOf } from "./Symbol";
4
+ export const versionTypes = [
5
+ /**
6
+ * The roll is in a state where it is used as
7
+ * the master roll for several new reproductions.
8
+ */
9
+ 'edition',
10
+ /**
11
+ * This denotes a version which is specific to (early)
12
+ * Welte-Mignon piano rolls, where rolls inteded to
13
+ * be pulished are revised by a controller first. These
14
+ * rolls typically carry a "controlliert" stamp. The
15
+ * revision is done on the same date as the perforation
16
+ * and the date is written on the roll towards its end.
17
+ */
18
+ 'authorised-revision',
19
+ /**
20
+ * Unauthorised revisions are those, which cannot be linked
21
+ * to a specific controller and are likely done by
22
+ * a later, anonymous hand.
23
+ */
24
+ 'unauthorised-revision',
25
+ /**
26
+ * In the case of Welte Mignon rolls, glosses are
27
+ * typically comments about the roll's condition, added
28
+ * e.g. by the collector.
29
+ */
30
+ 'gloss'
31
+ ];
32
+ export const traverseVersions = (version, callback) => {
33
+ callback(version);
34
+ if (version.basedOn) {
35
+ traverseVersions(flat(version.basedOn), callback);
36
+ }
37
+ };
38
+ export const getSnapshot = (version) => {
39
+ const snapshot = [];
40
+ const toDelete = [];
41
+ traverseVersions(version, s => {
42
+ snapshot.push(...s.edits.flatMap(edit => edit.insert || []));
43
+ // as we travel further up, remove symbols that are
44
+ // deleted in the versions further down
45
+ const deleted = [];
46
+ for (const toRemove of toDelete) {
47
+ const index = snapshot.findIndex(s => s === toRemove);
48
+ if (index !== -1) {
49
+ snapshot.splice(index, 1);
50
+ deleted.push(toRemove);
51
+ }
52
+ }
53
+ for (const del of deleted) {
54
+ toDelete.splice(toDelete.indexOf(del), 1);
55
+ }
56
+ // collect symbols that are deleted in the current version
57
+ toDelete.push(...s.edits.flatMap(edit => edit.delete || []));
58
+ });
59
+ return snapshot.sort((a, b) => {
60
+ const aDimension = dimensionOf(a);
61
+ const bDimension = dimensionOf(b);
62
+ return aDimension.horizontal.from - bDimension.horizontal.from;
63
+ });
64
+ };
65
+ const isCollatable = (symbolA, symbolB, tolerance = {
66
+ toleranceEnd: 5,
67
+ toleranceStart: 5
68
+ }) => {
69
+ // two symbols are collatible if they share the same
70
+ // symbol characteristics (pitch, expression type etc.)
71
+ // and occur in the same horizontal position.
72
+ if (symbolA.type === 'note' && symbolB.type === 'note') {
73
+ if (symbolA.pitch !== symbolB.pitch)
74
+ return false;
75
+ }
76
+ else if (symbolA.type === 'expression' && symbolB.type === 'expression') {
77
+ if (symbolA.expressionType !== symbolB.expressionType)
78
+ return false;
79
+ if (symbolA.scope !== symbolB.scope)
80
+ return false;
81
+ }
82
+ const dimensionA = dimensionOf(symbolA);
83
+ const dimensionB = dimensionOf(symbolB);
84
+ const distanceStart = Math.abs(dimensionA.horizontal.from - dimensionB.horizontal.from);
85
+ const distanceEnd = Math.abs(dimensionA.horizontal.to - dimensionB.horizontal.to);
86
+ if (distanceStart > tolerance.toleranceStart
87
+ || distanceEnd > tolerance.toleranceEnd) {
88
+ // the symbols are too far apart to be collated
89
+ return false;
90
+ }
91
+ return true;
92
+ };
93
+ const overlaps = (a, b) => {
94
+ const overlapsDimension = (a, b) => (a.from ?? 0) < (b.to ?? Infinity) && (b.from ?? 0) < (a.to ?? Infinity);
95
+ return overlapsDimension(a.horizontal, b.horizontal) && overlapsDimension(a.vertical, b.vertical);
96
+ };
97
+ /**
98
+ * Walks through the versions. If it finds a symbol that is still
99
+ * part of the tradition (i.e. it is included in the current snapshot)
100
+ * and is equivalent with the given symbol, it will add the feature
101
+ * carrying the symbol to the collated symbol. Otherwise, the
102
+ * given symbol will be added to the current version's insertions.
103
+ *
104
+ * All symbols of the tradition that are not included in the given
105
+ * symbols are considered to be deleted.
106
+ *
107
+ * @param creation
108
+ * @param symbols
109
+ * @returns
110
+ */
111
+ export function fillEdits(currentVersion, symbols, tolerance = {
112
+ toleranceEnd: 5,
113
+ toleranceStart: 5
114
+ }) {
115
+ const snapshot = getSnapshot(currentVersion);
116
+ const treatedSymbols = [];
117
+ // can it be collated with any of the symbols of
118
+ // included in the current snapshot?
119
+ const insertions = [...symbols];
120
+ for (const symbol of symbols) {
121
+ snapshot
122
+ .filter(toCompare => isCollatable(symbol, toCompare, tolerance))
123
+ .forEach(corresp => {
124
+ corresp.carriers.push(...symbol.carriers);
125
+ insertions.splice(insertions.indexOf(symbol), 1);
126
+ treatedSymbols.push(corresp);
127
+ });
128
+ }
129
+ // special treatment for covers
130
+ const covers = insertions.filter(symbol => symbol.type === 'cover');
131
+ for (const cover of covers) {
132
+ // find perforations in the snapshot that
133
+ // overlap with the cover
134
+ snapshot
135
+ .filter(symbol => symbol.type === 'note' || symbol.type === 'expression')
136
+ .map(dimensionOf)
137
+ .filter(dimension => overlaps(dimension, dimensionOf(cover)))
138
+ .forEach(dimension => {
139
+ const coverDimension = dimensionOf(cover);
140
+ // check if the cover partially covers the beginning
141
+ if (dimension.horizontal.from >= coverDimension.horizontal.from &&
142
+ dimension.horizontal.from <= coverDimension.horizontal.to) {
143
+ // the note starts where the cover ends
144
+ dimension.horizontal.from = coverDimension.horizontal.to;
145
+ }
146
+ // check if the cover partially covers the ending
147
+ if (dimension.horizontal.to >= coverDimension.horizontal.from &&
148
+ dimension.horizontal.to <= coverDimension.horizontal.to) {
149
+ // the note ends where the cover starts
150
+ dimension.horizontal.to = coverDimension.horizontal.from;
151
+ }
152
+ });
153
+ insertions.splice(insertions.indexOf(cover), 1);
154
+ }
155
+ currentVersion.edits.push(...insertions.map((symbol) => {
156
+ return {
157
+ insert: [symbol],
158
+ delete: [],
159
+ id: v4(),
160
+ };
161
+ }));
162
+ const deletions = snapshot.filter(sym => {
163
+ return !treatedSymbols.includes(sym);
164
+ });
165
+ for (const symbol of deletions) {
166
+ currentVersion.edits.push({
167
+ insert: [],
168
+ delete: [symbol],
169
+ id: v4(),
170
+ });
171
+ }
172
+ }
173
+ /**
174
+ * Assigns a generation (depth) to every node.
175
+ */
176
+ export function assignGenerations(versions) {
177
+ const byId = new Map();
178
+ for (const n of versions)
179
+ byId.set(n.id, n);
180
+ const memo = new Map(); // id -> generation
181
+ const inStack = new Set(); // for cycle detection
182
+ const computeGen = (id) => {
183
+ if (memo.has(id))
184
+ return memo.get(id);
185
+ if (inStack.has(id)) {
186
+ throw new Error(`Cycle detected involving node '${id}'. Check parentId links.`);
187
+ }
188
+ const node = byId.get(id);
189
+ if (!node) {
190
+ // If you ever call computeGen on a missing id, treat as root.
191
+ memo.set(id, 0);
192
+ return 0;
193
+ }
194
+ inStack.add(id);
195
+ let gen;
196
+ const p = node.basedOn?.assigned.id;
197
+ if (p === undefined) {
198
+ gen = 0; // root
199
+ }
200
+ else if (!byId.has(p)) {
201
+ // Orphaned parent reference — treat boundary as root
202
+ gen = 0;
203
+ }
204
+ else {
205
+ gen = 1 + computeGen(p);
206
+ }
207
+ inStack.delete(id);
208
+ memo.set(id, gen);
209
+ return gen;
210
+ };
211
+ // Compute for all nodes (order doesn’t matter)
212
+ const withGen = versions.map(n => ({
213
+ ...n,
214
+ generation: computeGen(n.id),
215
+ }));
216
+ return withGen;
217
+ }
@@ -0,0 +1,3 @@
1
+ export interface WithId {
2
+ 'id': string;
3
+ }
package/lib/WithId.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { RollFeature } from "./Feature";
2
+ type AlignmentResult = {
3
+ shift: number;
4
+ stretch: number;
5
+ };
6
+ /**
7
+ * Align two rolls by computing independent linear fits of each roll's note-onset positions
8
+ * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
9
+ */
10
+ export declare function alignFeatures(rollA: RollFeature[], rollB: RollFeature[]): AlignmentResult;
11
+ export {};
@@ -0,0 +1,54 @@
1
+ import { WelteT100 } from "./TrackerBar";
2
+ const isNote = (feature) => {
3
+ return new WelteT100().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) {
37
+ // 1. Extract note-onset positions
38
+ const allXA = rollA.filter(isNote).map(f => f.horizontal.from);
39
+ const allXB = rollB.filter(isNote).map(f => f.horizontal.from);
40
+ // 2. Determine segment size (e.g. 10% of notes, min 5)
41
+ const segCount = Math.max(5, Math.floor(allXA.length * 0.1));
42
+ // 3. Select only first and last segments
43
+ const XA = selectEnds(allXA, segCount);
44
+ const idxA = XA.map((_, i) => i);
45
+ const XB = selectEnds(allXB, segCount);
46
+ const idxB = XB.map((_, i) => i);
47
+ // 4. Fit index->position for each roll on selected ends
48
+ const { alpha: alphaA, beta: betaA } = fitIndexToPosition(idxA, XA);
49
+ const { alpha: alphaB, beta: betaB } = fitIndexToPosition(idxB, XB);
50
+ // 5. Derive stretch and shift such that x2 = (x1 + shift) * stretch
51
+ const stretch = alphaB / alphaA;
52
+ const shift = betaB / stretch - betaA;
53
+ return { stretch, shift };
54
+ }
@@ -0,0 +1,7 @@
1
+ import { AnySymbol } from "./Symbol";
2
+ type AlignmentResult = {
3
+ shift: number;
4
+ stretch: number;
5
+ };
6
+ export declare function alignSymbols(rollA: AnySymbol[], rollB: AnySymbol[]): AlignmentResult;
7
+ export {};