linked-rolls 0.10.1 → 0.12.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 (42) hide show
  1. package/lib/Collation.d.ts +1 -1
  2. package/lib/Collation.js +53 -14
  3. package/lib/EditionView.d.ts +11 -0
  4. package/lib/EditionView.js +78 -82
  5. package/lib/Emulation.js +10 -16
  6. package/lib/Quantity.d.ts +4 -0
  7. package/lib/Quantity.js +3 -0
  8. package/lib/RollCopy.d.ts +43 -1
  9. package/lib/TrackerBar.d.ts +16 -1
  10. package/lib/TrackerBar.js +21 -0
  11. package/lib/Version.d.ts +5 -0
  12. package/lib/Version.js +4 -0
  13. package/lib/alignment.d.ts +8 -8
  14. package/lib/alignment.js +15 -15
  15. package/lib/constraints.js +4 -2
  16. package/lib/editionOps.d.ts +13 -5
  17. package/lib/editionOps.js +87 -37
  18. package/lib/importJsonLd.d.ts +3 -1
  19. package/lib/importJsonLd.js +22 -42
  20. package/lib/index.d.ts +3 -1
  21. package/lib/index.js +3 -1
  22. package/lib/migrate.js +34 -6
  23. package/lib/readers/spencerBar.d.ts +34 -0
  24. package/lib/readers/spencerBar.js +141 -0
  25. package/lib/readers/stanfordAton.d.ts +9 -1
  26. package/lib/readers/stanfordAton.js +30 -24
  27. package/lib/schema.json +90 -1
  28. package/lib/sorted.d.ts +6 -0
  29. package/lib/sorted.js +17 -0
  30. package/lib/spec/context.json +11 -1
  31. package/lib/systems/index.d.ts +6 -0
  32. package/lib/systems/index.js +10 -0
  33. package/lib/systems/welteLicensee/bar.d.ts +11 -0
  34. package/lib/systems/welteLicensee/bar.js +38 -0
  35. package/lib/systems/welteT100/bar.d.ts +2 -1
  36. package/lib/systems/welteT100/bar.js +4 -2
  37. package/lib/systems/welteT100/system.js +5 -4
  38. package/package.json +1 -1
  39. package/lib/readers/midiSpans.d.ts +0 -25
  40. package/lib/readers/midiSpans.js +0 -112
  41. package/lib/readers/spencerMidi.d.ts +0 -27
  42. package/lib/readers/spencerMidi.js +0 -60
@@ -1,5 +1,6 @@
1
1
  import { v4 } from "uuid";
2
2
  import { AtonParser } from "./AtonParser";
3
+ import { systemOf, translationBetween } from "../TrackerBar";
3
4
  import { welteT100 } from "../systems/welteT100/bar";
4
5
  import { inMillimeters, mean, mm, px, subtract, track } from "../Quantity";
5
6
  /** Values in these files carry their unit as a suffix, e.g. "37.7646px". */
@@ -50,6 +51,10 @@ const gridOffsetOf = (holes, separation, stated) => {
50
51
  return readPx(stated);
51
52
  return px(median(holes.map(hole => readPx(hole.CENTROID_COL) - +hole.TRACKER_HOLE * separation)));
52
53
  };
54
+ const chainsAmong = (holes) => holes
55
+ .filter(hole => hole.NOTE_ATTACK && hole.OFF_TIME)
56
+ .map(hole => ({ hole, attack: readPx(hole.NOTE_ATTACK), release: readPx(hole.OFF_TIME) }))
57
+ .sort((a, b) => a.attack - b.attack);
53
58
  const punchDiameterOf = (holes, dpi) => {
54
59
  const circular = holes
55
60
  .filter(hole => parseFloat(hole.CIRCULARITY) > 0.95)
@@ -78,7 +83,7 @@ const measuredByOf = (rollinfo) => {
78
83
  date
79
84
  };
80
85
  };
81
- export function readFromStanfordAton(atonString, { trackShift, bar = welteT100, scan } = {}) {
86
+ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100, system = bar, scan } = {}) {
82
87
  const parser = new AtonParser();
83
88
  const json = parser.parse(atonString);
84
89
  const holes = json.ROLLINFO.HOLES.HOLE;
@@ -89,7 +94,7 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
89
94
  const measuredBy = measuredByOf(json.ROLLINFO);
90
95
  const rewindTrack = rewindTrackIn(holes);
91
96
  const shift = trackShift
92
- ?? (rewindTrack === undefined ? track(0) : track(bar.rewindTrack - rewindTrack));
97
+ ?? (rewindTrack === undefined ? track(0) : track(system.rewindTrack - rewindTrack));
93
98
  const calibration = {
94
99
  unit: 'px',
95
100
  offset: gridOffsetOf(holes, separation, json.ROLLINFO.HOLE_OFFSET),
@@ -97,31 +102,31 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
97
102
  shift
98
103
  };
99
104
  const punchDiameter = punchDiameterOf(holes, dpi);
100
- const chains = [...holes, ...chainedBadHoles(listOf(json.ROLLINFO.BADHOLES?.HOLE), calibration)]
101
- .filter(hole => hole.NOTE_ATTACK && hole.OFF_TIME)
102
- .sort((a, b) => readPx(a.NOTE_ATTACK) - readPx(b.NOTE_ATTACK));
105
+ const chains = chainsAmong([...holes, ...chainedBadHoles(listOf(json.ROLLINFO.BADHOLES?.HOLE), calibration)]);
106
+ const onBar = translationBetween(system, bar);
103
107
  const features = chains
104
- .map((hole) => {
105
- const attack = readPx(hole.NOTE_ATTACK);
106
- const release = readPx(hole.OFF_TIME);
108
+ .flatMap(({ hole, attack, release }) => {
109
+ const position = onBar(track(+hole.TRACKER_HOLE + shift));
110
+ if (position === undefined)
111
+ return [];
107
112
  const column = readPx(hole.ORIGIN_COL);
108
113
  const columnWidth = readPx(hole.WIDTH_COL);
109
- return {
110
- type: 'Hole',
111
- id: v4(),
112
- ...(stanford && {
113
- depiction: stanford.depictionOf(column, attack, columnWidth, subtract(release, attack))
114
- }),
115
- vertical: {
116
- from: track(+hole.TRACKER_HOLE + shift),
117
- unit: 'track'
118
- },
119
- horizontal: {
120
- unit: 'mm',
121
- from: inMillimeters(attack, dpi),
122
- to: inMillimeters(release, dpi)
123
- }
124
- };
114
+ return [{
115
+ type: 'Hole',
116
+ id: v4(),
117
+ ...(stanford && {
118
+ depiction: stanford.depictionOf(column, attack, columnWidth, subtract(release, attack))
119
+ }),
120
+ vertical: {
121
+ from: position,
122
+ unit: 'track'
123
+ },
124
+ horizontal: {
125
+ unit: 'mm',
126
+ from: inMillimeters(attack, dpi),
127
+ to: inMillimeters(release, dpi)
128
+ }
129
+ }];
125
130
  });
126
131
  return {
127
132
  type: 'RollCopy',
@@ -129,6 +134,7 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
129
134
  ops: [],
130
135
  conditions: [],
131
136
  keeper: { name: '', sameAs: [] },
137
+ production: { system: systemOf(system) },
132
138
  modifications: [],
133
139
  ...((scan ?? stanford) && { scan: scan ?? stanford?.scan }),
134
140
  measurements: {
package/lib/schema.json CHANGED
@@ -1239,6 +1239,83 @@
1239
1239
  ],
1240
1240
  "type": "object"
1241
1241
  },
1242
+ "ObjectAssumption<PaperSpeed>": {
1243
+ "anyOf": [
1244
+ {
1245
+ "properties": {
1246
+ "@annotation": {
1247
+ "description": "An optional annotation expressing a belief about this assumption. Uses the JSON-LD-star `@annotation` mechanism to attach epistemic metadata (certainty and reasons) to any triple.",
1248
+ "properties": {
1249
+ "belief": {
1250
+ "$ref": "#/definitions/Belief",
1251
+ "description": "The belief held about the annotated statement. [ontology: crminf:J4i is subject of]"
1252
+ },
1253
+ "@id": {
1254
+ "description": "A unique identifier for this object.",
1255
+ "type": "string"
1256
+ }
1257
+ },
1258
+ "required": [
1259
+ "belief",
1260
+ "@id"
1261
+ ],
1262
+ "type": "object"
1263
+ },
1264
+ "unit": {
1265
+ "const": "ft/min",
1266
+ "description": "The unit of measurement. [ontology: crm:P91 has unit]",
1267
+ "type": "string"
1268
+ },
1269
+ "value": {
1270
+ "$ref": "#/definitions/Quantity%3C%22ft%2Fmin%22%3E",
1271
+ "description": "The measured value. [ontology: crm:P90 has value]"
1272
+ }
1273
+ },
1274
+ "required": [
1275
+ "unit",
1276
+ "value"
1277
+ ],
1278
+ "type": "object"
1279
+ },
1280
+ {
1281
+ "properties": {
1282
+ "@annotation": {
1283
+ "description": "An optional annotation expressing a belief about this assumption. Uses the JSON-LD-star `@annotation` mechanism to attach epistemic metadata (certainty and reasons) to any triple.",
1284
+ "properties": {
1285
+ "belief": {
1286
+ "$ref": "#/definitions/Belief",
1287
+ "description": "The belief held about the annotated statement. [ontology: crminf:J4i is subject of]"
1288
+ },
1289
+ "@id": {
1290
+ "description": "A unique identifier for this object.",
1291
+ "type": "string"
1292
+ }
1293
+ },
1294
+ "required": [
1295
+ "belief",
1296
+ "@id"
1297
+ ],
1298
+ "type": "object"
1299
+ },
1300
+ "unit": {
1301
+ "const": "m/min",
1302
+ "description": "The unit of measurement. [ontology: crm:P91 has unit]",
1303
+ "type": "string"
1304
+ },
1305
+ "value": {
1306
+ "$ref": "#/definitions/Quantity%3C%22m%2Fmin%22%3E",
1307
+ "description": "The measured value. [ontology: crm:P90 has value]"
1308
+ }
1309
+ },
1310
+ "required": [
1311
+ "unit",
1312
+ "value"
1313
+ ],
1314
+ "type": "object"
1315
+ }
1316
+ ],
1317
+ "description": "An object assumption wraps a complex object with an optional annotation. Used for structured values (e.g. persons, conditions) whose properties may be uncertain."
1318
+ },
1242
1319
  "ObjectAssumption<Person>": {
1243
1320
  "description": "An object assumption wraps a complex object with an optional annotation. Used for structured values (e.g. persons, conditions) whose properties may be uncertain.",
1244
1321
  "properties": {
@@ -1462,7 +1539,7 @@
1462
1539
  "type": "object"
1463
1540
  },
1464
1541
  "ProductionEvent": {
1465
- "description": "Describes the production of a roll copy: the manufacturer, the paper used, and the date. [ontology: lrmoo:F32 Item Production Event]",
1542
+ "description": "Describes the production of a roll copy: the manufacturer, the paper used, the date, and the system and paper speed the copy was cut for. [ontology: lrmoo:F32 Item Production Event]",
1466
1543
  "properties": {
1467
1544
  "company": {
1468
1545
  "$ref": "#/definitions/Agent",
@@ -1475,6 +1552,14 @@
1475
1552
  "paper": {
1476
1553
  "$ref": "#/definitions/Concept",
1477
1554
  "description": "The paper the roll copy was cut on. [ontology: crm:P126 employed]"
1555
+ },
1556
+ "speed": {
1557
+ "$ref": "#/definitions/ObjectAssumption%3CPaperSpeed%3E",
1558
+ "description": "The paper speed the copy was cut for. A copy cut from the same master for another speed comes out longer or shorter than the roll it is aligned with by the ratio of the speeds, which is what the alignment then measures. [ontology: reo:paperSpeed]"
1559
+ },
1560
+ "system": {
1561
+ "$ref": "#/definitions/Concept",
1562
+ "description": "The reproducing system the copy was cut for. Left out, it is the roll's own; a Licensee re-cut of a T-100 roll names the Licensee here. A system the type vocabulary knows carries the IRI of its concept as `id`. [ontology: crm:P32 used general technique]"
1478
1563
  }
1479
1564
  },
1480
1565
  "type": "object"
@@ -1694,6 +1779,10 @@
1694
1779
  "$ref": "#/definitions/Measure%3C%22mm%22%3E",
1695
1780
  "description": "The average diameter of punched holes. [ontology: reo:punchDiameter]"
1696
1781
  },
1782
+ "scale": {
1783
+ "description": "The factor this copy's features were scaled by to align them with the others. What it is put down to is stated apart: a paper-stretch condition, or the speed the copy was cut for. Not exported to RDF.",
1784
+ "type": "number"
1785
+ },
1697
1786
  "shift": {
1698
1787
  "$ref": "#/definitions/Shift",
1699
1788
  "description": "The shift applied to align this copy with the others. Not exported to RDF."
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The index at which a list partitioned by the predicate turns: it
3
+ * holds for every item before that index and for none from it on.
4
+ * Binary search, so the list is read in logarithmic time.
5
+ */
6
+ export declare const partitionPoint: <T>(partitioned: readonly T[], holds: (item: T) => boolean) => number;
package/lib/sorted.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The index at which a list partitioned by the predicate turns: it
3
+ * holds for every item before that index and for none from it on.
4
+ * Binary search, so the list is read in logarithmic time.
5
+ */
6
+ export const partitionPoint = (partitioned, holds) => {
7
+ let low = 0;
8
+ let high = partitioned.length;
9
+ while (low < high) {
10
+ const middle = (low + high) >>> 1;
11
+ if (holds(partitioned[middle]))
12
+ low = middle + 1;
13
+ else
14
+ high = middle;
15
+ }
16
+ return low;
17
+ };
@@ -123,6 +123,7 @@
123
123
  "@id": "crm:P39i_was_measured_by",
124
124
  "@context": {
125
125
  "shift": null,
126
+ "scale": null,
126
127
  "trackCalibration": null,
127
128
  "margins": null,
128
129
  "dimensions": "reo:dimensions",
@@ -136,7 +137,16 @@
136
137
  "version": "owl:versionInfo"
137
138
  }
138
139
  },
139
- "production": "lrmoo:R28i_was_produced_by",
140
+ "production": {
141
+ "@id": "lrmoo:R28i_was_produced_by",
142
+ "@context": {
143
+ "system": "crm:P32_used_general_technique",
144
+ "speed": {
145
+ "@id": "reo:paperSpeed",
146
+ "@context": { "value": "crm:P90_has_value" }
147
+ }
148
+ }
149
+ },
140
150
  "company": "crm:P14_carried_out_by",
141
151
  "paper": "crm:P126_employed",
142
152
  "conditions": "crm:P44_has_condition",
@@ -0,0 +1,6 @@
1
+ import { Concept } from "../Agent";
2
+ import { TrackerBar } from "../TrackerBar";
3
+ /** The tracker bars the library knows, the T-100 first as the usual one. */
4
+ export declare const trackerBars: readonly TrackerBar[];
5
+ /** The bar of a system the type vocabulary knows, from its concept. */
6
+ export declare const trackerBarOf: (system: Concept | undefined) => TrackerBar | undefined;
@@ -0,0 +1,10 @@
1
+ import { systemIdOf } from "../TrackerBar";
2
+ import { welteT100 } from "./welteT100/bar";
3
+ import { welteLicensee } from "./welteLicensee/bar";
4
+ /** The tracker bars the library knows, the T-100 first as the usual one. */
5
+ export const trackerBars = [welteT100, welteLicensee];
6
+ /** The bar of a system the type vocabulary knows, from its concept. */
7
+ export const trackerBarOf = (system) => {
8
+ const id = systemIdOf(system);
9
+ return trackerBars.find(bar => bar.id === id);
10
+ };
@@ -0,0 +1,11 @@
1
+ import { TrackerBar } from "../../TrackerBar";
2
+ /**
3
+ * Welte-Mignon (Licensee), the American re-cut of the T-100 rolls on
4
+ * a roll 11¼ inches wide with 98 positions at nine to the inch, cf.
5
+ * Hagmann, p. 40 f. and Phillips, p. 123. It reads the same commands
6
+ * as the T-100 and lays them out the same way, minus the two motor
7
+ * tracks, so the note block and the treble valves sit two positions
8
+ * lower. The layout is the one Stanford's midi2exp reads, checked
9
+ * valve by valve on roll 225 against the T-100 copies.
10
+ */
11
+ export declare const welteLicensee: TrackerBar;
@@ -0,0 +1,38 @@
1
+ import { describeTrackerBar } from "../../TrackerBar";
2
+ import { mm } from "../../Quantity";
3
+ /**
4
+ * Welte-Mignon (Licensee), the American re-cut of the T-100 rolls on
5
+ * a roll 11¼ inches wide with 98 positions at nine to the inch, cf.
6
+ * Hagmann, p. 40 f. and Phillips, p. 123. It reads the same commands
7
+ * as the T-100 and lays them out the same way, minus the two motor
8
+ * tracks, so the note block and the treble valves sit two positions
9
+ * lower. The layout is the one Stanford's midi2exp reads, checked
10
+ * valve by valve on roll 225 against the T-100 copies.
11
+ */
12
+ export const welteLicensee = describeTrackerBar({
13
+ id: 'welte-licensee',
14
+ name: 'Welte-Mignon (Licensee)',
15
+ width: mm(285.75),
16
+ trackCount: 98,
17
+ notes: { from: 9, to: 88, lowestPitch: 24 },
18
+ expressions: new Map([
19
+ [1, 'MezzoforteOff'],
20
+ [2, 'MezzoforteOn'],
21
+ [3, 'SlowCrescendoOff'],
22
+ [4, 'SlowCrescendoOn'],
23
+ [5, 'ForzandoOff'],
24
+ [6, 'ForzandoOn'],
25
+ [7, 'SoftPedalOff'],
26
+ [8, 'SoftPedalOn'],
27
+ [89, 'Rewind'],
28
+ [90, 'ElectricCutOff'],
29
+ [91, 'SustainPedalOn'],
30
+ [92, 'SustainPedalOff'],
31
+ [93, 'ForzandoOn'],
32
+ [94, 'ForzandoOff'],
33
+ [95, 'SlowCrescendoOn'],
34
+ [96, 'SlowCrescendoOff'],
35
+ [97, 'MezzoforteOn'],
36
+ [98, 'MezzoforteOff']
37
+ ])
38
+ });
@@ -9,6 +9,7 @@ export type WelteT100ExpressionType = typeof welteT100ExpressionTypes[number];
9
9
  *
10
10
  * The note block spans 80 positions from C1 to g⁴, i.e. MIDI 24 to 103.
11
11
  * The expression valves are duplicated, bass below the note block and
12
- * treble above it, in mirrored order.
12
+ * treble above it, in mirrored order. The rolls run at three metres a
13
+ * minute (Phillips 2016, p. 113; Bärtsch 2020 gives 3 or 2.9).
13
14
  */
14
15
  export declare const welteT100: TrackerBar;
@@ -1,5 +1,5 @@
1
1
  import { describeTrackerBar } from "../../TrackerBar";
2
- import { mm } from "../../Quantity";
2
+ import { metersPerMinute, mm } from "../../Quantity";
3
3
  /**
4
4
  * The commands of the Welte-Mignon T-100, as its tracker bar reads them.
5
5
  */
@@ -24,7 +24,8 @@ export const welteT100ExpressionTypes = [
24
24
  *
25
25
  * The note block spans 80 positions from C1 to g⁴, i.e. MIDI 24 to 103.
26
26
  * The expression valves are duplicated, bass below the note block and
27
- * treble above it, in mirrored order.
27
+ * treble above it, in mirrored order. The rolls run at three metres a
28
+ * minute (Phillips 2016, p. 113; Bärtsch 2020 gives 3 or 2.9).
28
29
  */
29
30
  export const welteT100 = describeTrackerBar({
30
31
  id: 'welte-t100',
@@ -32,6 +33,7 @@ export const welteT100 = describeTrackerBar({
32
33
  width: mm(328),
33
34
  trackCount: 100,
34
35
  notes: { from: 11, to: 90, lowestPitch: 24 },
36
+ paperSpeed: { value: metersPerMinute(3), unit: 'm/min' },
35
37
  expressions: new Map([
36
38
  [1, 'MezzoforteOff'],
37
39
  [2, 'MezzoforteOn'],
@@ -1,6 +1,7 @@
1
1
  import { aperturePorts, CONSENSUS, DEFAULT_PUNCH_MM, geometryInMm, Grid, levelChanges, mezzoforteTravel, paperSeconds, pedalBrushing, pedalDefaults, pneumaticModel, PRESETS, ROWS_PER_MM, runPedals, TRACKER_BORE_MM, travelBetweenRails, WELTE_SPOOL, } from "welte-t100-emulator";
2
2
  import { welteT100 } from "./bar";
3
3
  import { add, inCentimeters, mm, seconds, track } from "../../Quantity";
4
+ import { partitionPoint } from "../../sorted";
4
5
  /**
5
6
  * The instruments the emulator was fitted as: the consensus over the six
6
7
  * rolls with drawn nuance lines, and the setting that drew each of them,
@@ -113,7 +114,7 @@ const velocityOf = (travel, hook, map) => {
113
114
  const gridOver = (events, spool) => {
114
115
  const last = mm(events.reduce((furthest, event) => Math.max(furthest, event.horizontal.to), 0));
115
116
  const length = Math.ceil(rowOf(add(last, RUN_OUT))) + 1;
116
- const times = Float64Array.from({ length }, (_, row) => secondsAt(spool, placeOfRow(row)));
117
+ const times = new Float64Array(length).map((_, row) => secondsAt(spool, placeOfRow(row)));
117
118
  return new Grid(0, times);
118
119
  };
119
120
  const nuanceCurves = (grid, ports, samples, options) => {
@@ -127,7 +128,7 @@ const nuanceCurves = (grid, ports, samples, options) => {
127
128
  name: half,
128
129
  kind: 'dynamics',
129
130
  travel,
130
- velocity: Float64Array.from(travel, value => velocityOf(value, hook, options.velocity))
131
+ velocity: travel.map(value => velocityOf(value, hook, options.velocity))
131
132
  };
132
133
  };
133
134
  return { bass: curveOf('bass'), treble: curveOf('treble') };
@@ -158,7 +159,7 @@ const performPedal = (type, curve, grid, readings, mode) => {
158
159
  if (readings.length === 0)
159
160
  return [];
160
161
  const ordered = readings.toSorted((a, b) => a.punch.rowOn - b.punch.rowOn);
161
- const causeOf = (row) => ordered[Math.max(ordered.findLastIndex(reading => reading.punch.rowOn <= row), 0)].event;
162
+ const causeOf = (row) => ordered[Math.max(partitionPoint(ordered, reading => reading.punch.rowOn <= row) - 1, 0)].event;
162
163
  return levelChanges(curve.travel, { mode })
163
164
  .filter(change => change.index > 0)
164
165
  .map(change => ({
@@ -182,7 +183,7 @@ const perform = (events, options, roll) => {
182
183
  const geometry = geometryInMm(roll.punchDiameter ?? options.punchDiameter, options.trackerBore);
183
184
  const ports = aperturePorts(grid, readings.map(reading => reading.punch), geometry);
184
185
  const samples = {
185
- place: Float64Array.from(grid.seconds, (_, row) => placeOfRow(row)),
186
+ place: grid.seconds.map((_, row) => placeOfRow(row)),
186
187
  seconds: grid.seconds
187
188
  };
188
189
  const nuance = nuanceCurves(grid, ports, samples, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,25 +0,0 @@
1
- import { MidiFile } from "midifile-ts";
2
- import { Milliseconds } from "../Quantity";
3
- export declare function midiTickToMilliseconds(ticks: number, microsecondsPerBeat: number, ppq: number): Milliseconds;
4
- interface Span<T extends string> {
5
- type: T;
6
- id: string;
7
- /** In ticks of the file. */
8
- onset: number;
9
- offset: number;
10
- onsetMs: Milliseconds;
11
- offsetMs: Milliseconds;
12
- link?: string;
13
- }
14
- export interface NoteSpan extends Span<'note'> {
15
- pitch: number;
16
- velocity: number;
17
- channel: number;
18
- }
19
- export interface SustainSpan extends Span<'sustain'> {
20
- }
21
- export interface SoftSpan extends Span<'soft'> {
22
- }
23
- export type AnySpan = NoteSpan | SustainSpan | SoftSpan;
24
- export declare const asSpans: (file: MidiFile, readLinks?: boolean) => AnySpan[];
25
- export {};
@@ -1,112 +0,0 @@
1
- import { MIDIControlEvents } from "midifile-ts";
2
- import { milliseconds } from "../Quantity";
3
- const isNoteOn = (event) => event.type === 'channel' && event.subtype === 'noteOn';
4
- const isNoteOff = (event) => event.type === 'channel' && event.subtype === 'noteOff';
5
- const isPedalOn = (event) => (event.type === 'channel'
6
- && event.subtype === 'controller'
7
- && event.controllerType === MIDIControlEvents.SUSTAIN
8
- && event.value > 63);
9
- const isPedalOff = (event) => (event.type === 'channel'
10
- && event.subtype === 'controller'
11
- && event.controllerType === MIDIControlEvents.SUSTAIN
12
- && event.value <= 63);
13
- const isSoftPedalOn = (event) => {
14
- return event.type === 'channel'
15
- && event.subtype === 'controller'
16
- && event.controllerType === MIDIControlEvents.SOFT_PEDAL
17
- && event.value > 63;
18
- };
19
- const isSoftPedalOff = (event) => {
20
- return event.type === 'channel'
21
- && event.subtype === 'controller'
22
- && event.controllerType === MIDIControlEvents.SOFT_PEDAL
23
- && event.value <= 63;
24
- };
25
- export function midiTickToMilliseconds(ticks, microsecondsPerBeat, ppq) {
26
- const beats = ticks / ppq;
27
- return milliseconds((beats * microsecondsPerBeat) / 1000);
28
- }
29
- export const asSpans = (file, readLinks = false) => {
30
- const resultingSpans = [];
31
- const tempoMap = [];
32
- const currentSpans = [];
33
- let bufferedMetaText;
34
- for (let i = 0; i < file.tracks.length; i++) {
35
- const track = file.tracks[i];
36
- let currentTime = 0;
37
- for (const event of track) {
38
- currentTime += event.deltaTime;
39
- if (event.type === 'meta' && event.subtype === 'setTempo') {
40
- tempoMap.push({
41
- atTick: currentTime,
42
- microsecondsPerBeat: event.microsecondsPerBeat
43
- });
44
- }
45
- if (readLinks && event.type === 'meta' && event.subtype === 'text') {
46
- bufferedMetaText = event.text;
47
- }
48
- else if (isNoteOn(event) || isPedalOn(event) || isSoftPedalOn(event)) {
49
- const type = isNoteOn(event) ? 'note' : isPedalOn(event) ? 'sustain' : 'soft';
50
- const currentTempo = tempoMap.slice().reverse().find(tempo => tempo.atTick <= currentTime);
51
- if (!currentTempo) {
52
- console.log('No tempo event found. Skipping');
53
- continue;
54
- }
55
- const onsetMs = midiTickToMilliseconds(currentTime, currentTempo.microsecondsPerBeat, file.header.ticksPerBeat);
56
- const link = bufferedMetaText;
57
- if (type === 'note') {
58
- const pitch = event.noteNumber;
59
- currentSpans.push({
60
- type,
61
- id: `${i}-${currentTime}-${pitch}`,
62
- onset: currentTime,
63
- offset: 0,
64
- velocity: event.velocity,
65
- pitch,
66
- channel: i,
67
- onsetMs,
68
- offsetMs: milliseconds(0),
69
- link
70
- });
71
- }
72
- else {
73
- currentSpans.push({
74
- type,
75
- id: `${i}-${currentTime}-${type}`,
76
- onset: currentTime,
77
- offset: 0,
78
- onsetMs,
79
- offsetMs: milliseconds(0),
80
- link
81
- });
82
- }
83
- bufferedMetaText = undefined;
84
- }
85
- else if (isNoteOff(event) || isPedalOff(event) || isSoftPedalOff(event)) {
86
- const type = isNoteOff(event) ? 'note' : isPedalOff(event) ? 'sustain' : 'soft';
87
- const currentTempo = tempoMap.slice().reverse().find(tempo => tempo.atTick <= currentTime);
88
- if (!currentTempo) {
89
- console.log('No tempo event found. Skipping');
90
- continue;
91
- }
92
- const counterpart = isNoteOff(event)
93
- ? currentSpans.find(e => e.type === 'note' && e.pitch === event.noteNumber)
94
- : currentSpans.find(e => e.type === type);
95
- if (!counterpart) {
96
- console.log('Found an off event of type', type, 'at', currentTime, 'without a previous on.', 'Event:', event, 'Current spans: ', currentSpans.map(span => span.type).join(' '));
97
- continue;
98
- }
99
- counterpart.offset = currentTime;
100
- counterpart.offsetMs = midiTickToMilliseconds(currentTime, currentTempo.microsecondsPerBeat, file.header.ticksPerBeat);
101
- if (bufferedMetaText && counterpart.link) {
102
- counterpart.link += ` ${bufferedMetaText}`;
103
- }
104
- resultingSpans.push(counterpart);
105
- currentSpans.splice(currentSpans.indexOf(counterpart), 1);
106
- }
107
- }
108
- }
109
- return resultingSpans
110
- .filter(span => span.offsetMs > span.onsetMs)
111
- .sort((a, b) => a.onset - b.onset);
112
- };
@@ -1,27 +0,0 @@
1
- import { RollCopy } from "../RollCopy";
2
- import { FeetPerMinute, Millimeters, Seconds, Track } from "../Quantity";
3
- /**
4
- * How a MIDI key number in one of Spencer Chase's roll files names a
5
- * tracker bar track.
6
- *
7
- * The note block follows the obvious rule, `pitch - 13`, which puts
8
- * track 11 on MIDI 24 as the T100 compass requires. The bass expression
9
- * block does not: it reads two tracks high, and subtracting two is what
10
- * has made these files come out right so far.
11
- *
12
- * The boundary between the two rules is unresolved. Taken literally the
13
- * rules leave tracks 8 and 9 unreachable and jump from track 7 to track 10,
14
- * which no lateral offset can produce, so at least one of them is
15
- * approximate. Settling it needs a Spencer file whose expression holes
16
- * can be checked against the roll, hence the option to override.
17
- */
18
- export declare const spencerTrackOf: (pitch: number) => Track;
19
- /**
20
- * Spencer Chase's rolls seem to be scanned at a roll speed of
21
- * 83 (=8.3 feet per minute). A scanner feeds the paper at one
22
- * speed, so time in his files is proportional to place.
23
- */
24
- export declare const SPENCER_FEET_PER_MINUTE: import("..").Quantity<"ft/min">;
25
- /** Place on the roll after `time` at a constant `speed`. */
26
- export declare const atConstantSpeed: (speed: FeetPerMinute) => (time: Seconds) => Millimeters;
27
- export declare function readFromSpencerMIDI(midiBuffer: ArrayBuffer, placeAt?: (time: Seconds) => Millimeters, trackOf?: (pitch: number) => Track): RollCopy;
@@ -1,60 +0,0 @@
1
- import { v4 } from "uuid";
2
- import { read } from "midifile-ts";
3
- import { asSpans } from "./midiSpans";
4
- import { feetPerMinute, inSeconds, mm, track } from "../Quantity";
5
- /**
6
- * How a MIDI key number in one of Spencer Chase's roll files names a
7
- * tracker bar track.
8
- *
9
- * The note block follows the obvious rule, `pitch - 13`, which puts
10
- * track 11 on MIDI 24 as the T100 compass requires. The bass expression
11
- * block does not: it reads two tracks high, and subtracting two is what
12
- * has made these files come out right so far.
13
- *
14
- * The boundary between the two rules is unresolved. Taken literally the
15
- * rules leave tracks 8 and 9 unreachable and jump from track 7 to track 10,
16
- * which no lateral offset can produce, so at least one of them is
17
- * approximate. Settling it needs a Spencer file whose expression holes
18
- * can be checked against the roll, hence the option to override.
19
- */
20
- export const spencerTrackOf = (pitch) => {
21
- const position = pitch - 13;
22
- return track(position < 10 ? position - 2 : position);
23
- };
24
- const MM_PER_FOOT = 304.8;
25
- const SECONDS_PER_MINUTE = 60;
26
- /**
27
- * Spencer Chase's rolls seem to be scanned at a roll speed of
28
- * 83 (=8.3 feet per minute). A scanner feeds the paper at one
29
- * speed, so time in his files is proportional to place.
30
- */
31
- export const SPENCER_FEET_PER_MINUTE = feetPerMinute(8.3);
32
- /** Place on the roll after `time` at a constant `speed`. */
33
- export const atConstantSpeed = (speed) => (time) => mm(speed * MM_PER_FOOT / SECONDS_PER_MINUTE * time);
34
- export function readFromSpencerMIDI(midiBuffer, placeAt = atConstantSpeed(SPENCER_FEET_PER_MINUTE), trackOf = spencerTrackOf) {
35
- const features = asSpans(read(midiBuffer))
36
- .filter(span => span.type === 'note')
37
- .map((span) => ({
38
- type: 'Hole',
39
- id: v4(),
40
- vertical: {
41
- from: trackOf(span.pitch),
42
- unit: 'track'
43
- },
44
- horizontal: {
45
- from: placeAt(inSeconds(span.onsetMs)),
46
- to: placeAt(inSeconds(span.offsetMs)),
47
- unit: 'mm'
48
- }
49
- }));
50
- return {
51
- type: 'RollCopy',
52
- id: v4(),
53
- ops: [],
54
- conditions: [],
55
- keeper: { name: '', sameAs: [] },
56
- measurements: {},
57
- modifications: [],
58
- features
59
- };
60
- }