discombobulator 1.0.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 (53) hide show
  1. package/package.json +16 -0
  2. package/src/articulation.mjs +61 -0
  3. package/src/articulationset.mjs +20 -0
  4. package/src/chord.mjs +82 -0
  5. package/src/cross.mjs +133 -0
  6. package/src/disco.mjs +14 -0
  7. package/src/element.mjs +51 -0
  8. package/src/group.mjs +70 -0
  9. package/src/groupset.mjs +259 -0
  10. package/src/instrument.mjs +105 -0
  11. package/src/instrumentset.mjs +20 -0
  12. package/src/irregulargroupset.mjs +224 -0
  13. package/src/note.mjs +155 -0
  14. package/src/noteset.mjs +21 -0
  15. package/src/pattern.mjs +52 -0
  16. package/src/patternset.mjs +20 -0
  17. package/src/patternsource.mjs +21 -0
  18. package/src/permute.mjs +63 -0
  19. package/src/regulargroupset.mjs +120 -0
  20. package/src/schedule.mjs +88 -0
  21. package/src/schema.mjs +108 -0
  22. package/src/set.mjs +1159 -0
  23. package/src/test/articulation.mjs +66 -0
  24. package/src/test/articulationset.mjs +43 -0
  25. package/src/test/binaryoperator.mjs +210 -0
  26. package/src/test/chord.mjs +292 -0
  27. package/src/test/concolour.mjs +70 -0
  28. package/src/test/difference.mjs +492 -0
  29. package/src/test/element.mjs +66 -0
  30. package/src/test/group.mjs +79 -0
  31. package/src/test/groupset.mjs +43 -0
  32. package/src/test/instrument.mjs +66 -0
  33. package/src/test/instrumentset.mjs +43 -0
  34. package/src/test/intersection.mjs +493 -0
  35. package/src/test/irregulargroupset.mjs +461 -0
  36. package/src/test/note.mjs +367 -0
  37. package/src/test/noteset.mjs +43 -0
  38. package/src/test/operator.mjs +63 -0
  39. package/src/test/pattern.mjs +113 -0
  40. package/src/test/patternset.mjs +43 -0
  41. package/src/test/patternsource.mjs +43 -0
  42. package/src/test/regulargroupset.mjs +290 -0
  43. package/src/test/repl.mjs +63 -0
  44. package/src/test/replinit.mjs +134 -0
  45. package/src/test/rotation.mjs +753 -0
  46. package/src/test/schedule.mjs +275 -0
  47. package/src/test/schema.mjs +333 -0
  48. package/src/test/set.mjs +1081 -0
  49. package/src/test/symmetricdifference.mjs +495 -0
  50. package/src/test/test.mjs +208 -0
  51. package/src/test/testing.mjs +63 -0
  52. package/src/test/union.mjs +495 -0
  53. package/src/util.mjs +21 -0
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "discombobulator",
3
+ "version": "1.0.0",
4
+ "description": "Discombobulator core library",
5
+ "license": "ISC",
6
+ "author": "Mark Webb",
7
+ "type": "module",
8
+ "main": "disco.mjs",
9
+ "scripts": {
10
+ "test": "node ./src/test/test.mjs -vs"
11
+ },
12
+ "dependencies": {
13
+ "commander": "^15.0.0",
14
+ "fraction.js": "^5.3.4"
15
+ }
16
+ }
@@ -0,0 +1,61 @@
1
+ // articulation.mjs
2
+ //
3
+ // An articulation is a modification to a note, such as an accent or roll.
4
+
5
+ import util from "util";
6
+
7
+ import { Element } from "./element.mjs";
8
+
9
+ export class Articulation extends Element {
10
+ #symbol = undefined; // Symbol representing the articulation.
11
+
12
+ constructor(id) {
13
+ super();
14
+
15
+ if (typeof id === 'string' || id instanceof String) {
16
+ this.#symbol = Symbol.for(id);
17
+ }
18
+ }
19
+
20
+ get [Symbol.toStringTag]() {
21
+ return "Articulation";
22
+ }
23
+
24
+ [util.inspect.custom](depth, opts) {
25
+ return `${this[Symbol.toStringTag]} {${
26
+ !!this.#symbol ? this.#symbol.toString() : "undefined"
27
+ }}`;
28
+ }
29
+
30
+ get symbol() { return this.#symbol; }
31
+
32
+ isEqual(other) {
33
+ return super.isEqual(other) && this.#symbol === other.#symbol;
34
+ }
35
+ }
36
+
37
+ // Library of defined articulations.
38
+ export class Articulations {
39
+ static Accent = new Articulation("accent"); // >
40
+ static Expressivo = new Articulation("expressivo"); // <>
41
+ static Marcato = new Articulation("marcato"); // ^
42
+ static Portato = new Articulation("portato"); // _
43
+ static Staccatissimo = new Articulation("staccatissimo"); // !
44
+ static Staccato = new Articulation("staccato"); // .
45
+ static Tenuto = new Articulation("tenuto"); // -
46
+ static VeryShortFermata = new Articulation("veryshortfermata");
47
+ static ShortFermata = new Articulation("shortfermata");
48
+ static Fermata = new Articulation("fermata");
49
+ static LongFermata = new Articulation("longfermata");
50
+ static VeryLongFermata = new Articulation("verylongfermata");
51
+ static HenzeShortFermata = new Articulation("henzeshortfermata");
52
+ static HenzeLongFermata = new Articulation("henzelongfermata");
53
+ static Roll8 = new Articulation("roll8");
54
+ static Roll16 = new Articulation("roll16");
55
+ static Roll32 = new Articulation("roll32");
56
+ static Ghost = new Articulation("ghost");
57
+ static RimShot = new Articulation("rimshot");
58
+ static Open = new Articulation("open");
59
+ static HalfOpen = new Articulation("halfopen");
60
+ static Stopped = new Articulation("stopped");
61
+ }
@@ -0,0 +1,20 @@
1
+ // ArticulationSet
2
+ //
3
+ // A set of articulations.
4
+
5
+ import { Set } from './set.mjs'
6
+ import { Articulation } from './articulation.mjs'
7
+
8
+ export class ArticulationSet extends Set {
9
+ constructor(options = {}) {
10
+ super(options)
11
+ }
12
+
13
+ get allowedElements() {
14
+ return [ Articulation ];
15
+ }
16
+
17
+ get [Symbol.toStringTag]() {
18
+ return "ArticulationSet";
19
+ }
20
+ }
package/src/chord.mjs ADDED
@@ -0,0 +1,82 @@
1
+ // Chord.
2
+ //
3
+ // A chord is a set of notes. An empty chord represents a rest.
4
+
5
+ import { Note } from "./note.mjs";
6
+ import { PatternSource } from "./patternsource.mjs";
7
+ import { NoteSet } from "./noteset.mjs";
8
+
9
+ export class Chord extends PatternSource {
10
+ static rest = new Chord();
11
+
12
+ constructor(options = {}) {
13
+ super({ ...options, strict: false });
14
+ }
15
+
16
+ get [Symbol.toStringTag]() {
17
+ return "Chord";
18
+ }
19
+
20
+ // Utility method to represent a chord for display, diagnostic and comparison purposes.
21
+ static summarise(chord) {
22
+ let articulations = [];
23
+ let instruments = [];
24
+
25
+ for (let note of chord) {
26
+ if (note.instrument) {
27
+ instruments.push(note.instrument);
28
+ }
29
+
30
+ if (note.articulations) {
31
+ for (let articulation of note.articulations) {
32
+ articulations.push(articulation);
33
+ }
34
+ }
35
+ }
36
+
37
+ return { instruments: instruments, articulations: articulations };
38
+ }
39
+
40
+ // Equality operator overide.
41
+ //
42
+ // Two chords are equal if their Notes result in the same set of Instruments
43
+ // and the same set of Articulations, exactly.
44
+ isEqual(other) {
45
+ if (!super.isEqual(other)) return false;
46
+
47
+ let a = Chord.summarise(this);
48
+ let b = Chord.summarise(other);
49
+
50
+ if (
51
+ a.instruments.length != b.instruments.length ||
52
+ a.articulations.size != b.articulations.size
53
+ ) {
54
+ return false;
55
+ }
56
+
57
+ for (let instrument of a.instruments) {
58
+ if (!b.instruments.includes(instrument)) return false;
59
+ }
60
+
61
+ for (let articulation of a.articulations) {
62
+ if (!b.articulations.includes(articulation)) return false;
63
+ }
64
+
65
+ return true;
66
+ }
67
+
68
+ // Overide base implementation to force semantics on Note to copy and merge.
69
+ internalAdd(element) {
70
+ let found = super.internalAdd(element, false);
71
+
72
+ if (found >= 0) {
73
+ let existing = this.elements[found];
74
+ this.elements[found] = new Note({ add: [ existing.instrument, existing.articulations ]});
75
+ this.elements[found].merge(element);
76
+ }
77
+ }
78
+
79
+ toNoteSet() {
80
+ return new NoteSet({add: this});
81
+ }
82
+ }
package/src/cross.mjs ADDED
@@ -0,0 +1,133 @@
1
+ // Factory.
2
+ //
3
+ // TEST factory to create sets dynamically at runtime.
4
+
5
+ import { Set } from './set.mjs'
6
+ import { InstrumentSet } from './instrumentset.mjs'
7
+ import { ArticulationSet } from './articulationset.mjs'
8
+ import { Note } from './note.mjs'
9
+ import { Chord } from './chord.mjs'
10
+ import { NoteSet } from './noteset.mjs'
11
+ import { Pattern } from './pattern.mjs'
12
+ import { PatternSet } from './patternset.mjs'
13
+
14
+ // Cross (Cartesian Product): All pairs of elements innerElement2-element setfrom two sets.
15
+ export class Cross extends Set.Operator {
16
+ // Map cross parameter types (sets) to a resultant set type that Cross acts as
17
+ // a proxy for. Entries in the map have the following fields:
18
+ // a: first set argument type in the cross.
19
+ // b: second set argument type in the cross. If a and b are different, and no d
20
+ // is provided, the element type of the crossed set will be a.
21
+ // c: the set class that the a X b would result in.
22
+ // d: [optional] If present, the elements of the resulting set are created as
23
+ // this type. This is helpful when the element type needs to be explicit,
24
+ // or their is no existing set type that naturally follows from the cross.
25
+ static crossMap = [
26
+ { a: InstrumentSet, b: InstrumentSet, c: Pattern, d: NoteSet },
27
+ { a: ArticulationSet, b: ArticulationSet, c: Pattern, d: NoteSet },
28
+ { a: InstrumentSet, b: ArticulationSet, c: NoteSet, d: Note },
29
+ { a: NoteSet, b: Chord, c: Pattern },
30
+ { a: Chord, b: Chord, c: Pattern },
31
+ { a: NoteSet, b: NoteSet, c: Pattern },
32
+ { a: Pattern, b: Pattern, c: PatternSet },
33
+ ];
34
+
35
+ #allowed;
36
+ #proxyfor;
37
+
38
+ static checkSetOf(set1SetOf, set2SetOf, a, b) {
39
+ const aSetOf = (new a()).asSetOf(true);
40
+ const bSetOf = (new b()).asSetOf(true);
41
+
42
+ return set1SetOf.equals(aSetOf) && set2SetOf.equals(bSetOf)
43
+ || set2SetOf.equals(aSetOf) && set1SetOf.equals(bSetOf);
44
+ }
45
+
46
+ constructor(set1, set2, options = {}) {
47
+ super(set1, set2, { ...options, skipValidation: true });
48
+
49
+ let promotion = Cross.crossMap.find(
50
+ (e) => set1 instanceof e.a && set2 instanceof e.b
51
+ || set2 instanceof e.a && set1 instanceof e.b
52
+ );
53
+
54
+ if (promotion === undefined) {
55
+ // Couldn't find a match based on set class type, try using SetOf.
56
+ const set1SetOf = set1.asSetOf(true);
57
+ const set2SetOf = set2.asSetOf(true);
58
+
59
+ promotion = Cross.crossMap.find(
60
+ (e) => Cross.checkSetOf(set1SetOf, set2SetOf, e.a, e.b)
61
+ );
62
+ }
63
+
64
+ if (promotion !== undefined) {
65
+ if (promotion.d === undefined) {
66
+ this.#allowed = [ promotion.a ];
67
+ if (promotion.a !== promotion.b) this.#allowed.push(promotion.b);
68
+ } else {
69
+ this.#allowed = [ promotion.d ];
70
+ }
71
+
72
+ this.#proxyfor = promotion.c;
73
+ } else {
74
+ throw new Error("No valid promotion for Cross");
75
+ }
76
+ }
77
+
78
+ get allowedElements() {
79
+ return this.#allowed;
80
+ }
81
+
82
+ get proxyFor() {
83
+ return this.#proxyfor;
84
+ }
85
+
86
+ get [Symbol.toStringTag]() {
87
+ return `Cross (${this.#proxyfor.name})`;
88
+ }
89
+
90
+ *[Symbol.iterator]() {
91
+ let element = this.#allowed[0];
92
+ let outer = this.elements[0];
93
+ let inner = this.elements[1];
94
+
95
+ for (let outerElement of outer) {
96
+ for (let innerElement of inner) {
97
+ yield new element({add: [ outerElement, innerElement ]});
98
+ }
99
+ }
100
+ }
101
+
102
+ get size() {
103
+ return this.elements[0].size * this.elements[1].size;
104
+ }
105
+
106
+ elementAt(index = 0) {
107
+ let firstSize = this.elements[0].size;
108
+ let secondSize = this.elements[1].size;
109
+ let element = this.#allowed[0];
110
+
111
+ if (index >= 0 && index < firstSize * secondSize) {
112
+ let firstIndex = Math.floor(index / secondSize);
113
+ let secondIndex = index % secondSize;
114
+
115
+ return new element(
116
+ {add: [
117
+ this.elements[0].elementAt(firstIndex),
118
+ this.elements[1].elementAt(secondIndex)
119
+ ]}
120
+ );
121
+ }
122
+
123
+ throw new Error("Invalid index");
124
+ }
125
+
126
+ has(element) {
127
+ if (!this.allowedElements.includes(element.constructor)) {
128
+ throw new Error("Invalid element - must be a compatible 2-element set");
129
+ }
130
+
131
+ return this.elements[0].has(element.elementAt(0)) && this.elements[1].has(element.elementAt(1));
132
+ }
133
+ };
package/src/disco.mjs ADDED
@@ -0,0 +1,14 @@
1
+ // disco.mjs
2
+ //
3
+ // Startup script to initialise package.
4
+
5
+ import { Cross } from "./cross.mjs"
6
+ import { Set } from "./set.mjs"
7
+
8
+ // Inject createCross into Set as a static method to allow runtime creation of
9
+ // Cross from Set so that Cross can automatically create result sets.
10
+ // Set.createCross = (set1, set2) => { return new Cross(set1, set2) };
11
+
12
+ export function useDisco() {
13
+ Set.createCross = (set1, set2) => { return new Cross(set1, set2) };
14
+ }
@@ -0,0 +1,51 @@
1
+ // Element.
2
+ //
3
+ // Base class for anything that can be contained in a Set.
4
+
5
+ export class Element {
6
+ constructor() {}
7
+
8
+ get [Symbol.toStringTag]() {
9
+ return "Element";
10
+ }
11
+
12
+ // Base class equality operator.
13
+ //
14
+ // Derived classes should overide this method and call super().isEqual() before
15
+ // performing type-specific equality testing.
16
+ //
17
+ // The base implementation simply checks that the RHS is defined and of the
18
+ // same class as this one (ie.both LHS and RHS must be the same types).
19
+ isEqual(other) {
20
+ if (!!other && this.constructor === other.constructor) return true;
21
+
22
+ return false;
23
+ }
24
+
25
+ // If possible and necessary, promotes the given element to the same
26
+ // type as this element.
27
+ //
28
+ // If the given element is already of the same type as this one, then
29
+ // the given element is returned unchanged.
30
+ //
31
+ // If the element can be promoted by the element, then it is used to
32
+ // initialise or modify this one to reflect the promotion. This is highly
33
+ // implementation dependant. eg. A set might just add the element to itself.
34
+ //
35
+ // If the element cannot be promoted, undefined is returned.
36
+ promote(element) {
37
+ if (this.constructor === element.constructor) {
38
+ return element;
39
+ }
40
+
41
+ return undefined;
42
+ }
43
+
44
+ // Merges the other element with this one.
45
+ //
46
+ // By default no merge takes place. ie. The other element is ignored.
47
+ //
48
+ // If a derived class, such as Note, has partial identity then it must overide this
49
+ // method to allow the non-identity parts (ArticulationSet in this case) to be merged.
50
+ merge(other) {}
51
+ }
package/src/group.mjs ADDED
@@ -0,0 +1,70 @@
1
+ // group.mjs
2
+ //
3
+ // A group is a time-axis pattern that specifies how chords may be grouped.
4
+
5
+ import util from "util";
6
+
7
+ import { Element } from "./element.mjs";
8
+
9
+ export class Group extends Element {
10
+ // A group is represented as a bit pattern beginning at bit 0 and extending
11
+ // for length bits (inclusive). ie. the bits in the range 0...length-1.
12
+ // In that range a 1 represents an active chord position, and 0 a rest.
13
+ #group = 0; // Binary group starting at bit 0.
14
+ #length = 0; // Length of the group [1...32], where 0 is undefined.
15
+
16
+ static MAX_LENGTH = 32;
17
+
18
+ constructor(group = 0, length = 0) {
19
+ super();
20
+
21
+ if (1 > length || length > Group.MAX_LENGTH) {
22
+ throw new Error(
23
+ `Invalid Group length - must be from 1 to ${Group.MAX_LENGTH}`
24
+ );
25
+ }
26
+
27
+ this.#group = length < Group.MAX_LENGTH ? group & ((1 << length) - 1) : group;
28
+ this.#length = length;
29
+ }
30
+
31
+ get [Symbol.toStringTag]() {
32
+ return "Group";
33
+ }
34
+
35
+ get group() {
36
+ return this.#group;
37
+ }
38
+ get length() {
39
+ return this.#length;
40
+ }
41
+
42
+ // Describe the group as a string.
43
+ // Note: the description is in the order LSB...MSB
44
+ // (the reverse of standard binary representation)
45
+ #describe() {
46
+ if (this.#length == 0) return "!";
47
+
48
+ let group = this.#group;
49
+ let description = "";
50
+
51
+ for (let i = 0; i < this.#length; ++i) {
52
+ description += group & 1 ? "#" : "-";
53
+ group >>= 1;
54
+ }
55
+
56
+ return description;
57
+ }
58
+
59
+ [util.inspect.custom](depth, opts) {
60
+ return `${this[Symbol.toStringTag]} {${this.#describe()}}`;
61
+ }
62
+
63
+ isEqual(other) {
64
+ return (
65
+ super.isEqual(other) &&
66
+ this.#length === other.#length &&
67
+ this.#group === other.#group
68
+ );
69
+ }
70
+ }