linked-rolls 0.56.0 → 0.58.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 (131) hide show
  1. package/README.md +54 -361
  2. package/lib/{constraints.d.ts → analysis/constraints.d.ts} +1 -1
  3. package/lib/{constraints.js → analysis/constraints.js} +9 -8
  4. package/lib/analysis/ownPaper.d.ts +26 -0
  5. package/lib/analysis/ownPaper.js +41 -0
  6. package/lib/{reservations.d.ts → analysis/reservations.d.ts} +3 -3
  7. package/lib/{reservations.js → analysis/reservations.js} +3 -3
  8. package/lib/{sigla.d.ts → analysis/sigla.d.ts} +2 -2
  9. package/lib/{sigla.js → analysis/sigla.js} +4 -4
  10. package/lib/{witnesses.d.ts → analysis/witnesses.d.ts} +2 -2
  11. package/lib/{witnesses.js → analysis/witnesses.js} +4 -4
  12. package/lib/{Collation.d.ts → collation/Collation.d.ts} +4 -4
  13. package/lib/{Collation.js → collation/Collation.js} +4 -4
  14. package/lib/{alignment.d.ts → collation/alignment.d.ts} +4 -4
  15. package/lib/{alignment.js → collation/alignment.js} +10 -16
  16. package/lib/{scatter.d.ts → collation/scatter.d.ts} +4 -4
  17. package/lib/{scatter.js → collation/scatter.js} +6 -6
  18. package/lib/{statistics.d.ts → collation/statistics.d.ts} +1 -1
  19. package/lib/{statistics.js → collation/statistics.js} +2 -2
  20. package/lib/{substitution.d.ts → collation/substitution.d.ts} +9 -24
  21. package/lib/{substitution.js → collation/substitution.js} +13 -37
  22. package/lib/emulation/Emulation.d.ts +64 -0
  23. package/lib/emulation/Emulation.js +282 -0
  24. package/lib/emulation/negotiation.d.ts +14 -0
  25. package/lib/emulation/negotiation.js +20 -0
  26. package/lib/index.d.ts +43 -35
  27. package/lib/index.js +36 -35
  28. package/lib/{asJsonLd.d.ts → io/asJsonLd.d.ts} +1 -1
  29. package/lib/{asJsonLd.js → io/asJsonLd.js} +22 -51
  30. package/lib/{context.d.ts → io/context.d.ts} +4 -4
  31. package/lib/{context.js → io/context.js} +4 -4
  32. package/lib/{importJsonLd.d.ts → io/importJsonLd.d.ts} +1 -1
  33. package/lib/io/importJsonLd.js +128 -0
  34. package/lib/{migrate.d.ts → io/migrate.d.ts} +9 -0
  35. package/lib/{migrate.js → io/migrate.js} +97 -54
  36. package/lib/{Agent.d.ts → model/Agent.d.ts} +1 -1
  37. package/lib/{Assumption.d.ts → model/Assumption.d.ts} +1 -1
  38. package/lib/{ConditionState.d.ts → model/ConditionState.d.ts} +1 -2
  39. package/lib/{Edit.d.ts → model/Edit.d.ts} +1 -1
  40. package/lib/{Edition.d.ts → model/Edition.d.ts} +2 -2
  41. package/lib/{Feature.d.ts → model/Feature.d.ts} +3 -1
  42. package/lib/{Feature.js → model/Feature.js} +2 -0
  43. package/lib/{FeatureSource.d.ts → model/FeatureSource.d.ts} +1 -1
  44. package/lib/{Perforator.d.ts → model/Perforator.d.ts} +1 -1
  45. package/lib/{RollCopy.d.ts → model/RollCopy.d.ts} +5 -12
  46. package/lib/{RollCopy.js → model/RollCopy.js} +3 -5
  47. package/lib/{Symbol.d.ts → model/Symbol.d.ts} +1 -1
  48. package/lib/{Version.d.ts → model/Version.d.ts} +11 -3
  49. package/lib/{Version.js → model/Version.js} +24 -1
  50. package/lib/{vocabulary.js → model/vocabulary.js} +2 -2
  51. package/lib/ops/beliefs.d.ts +9 -0
  52. package/lib/ops/beliefs.js +31 -0
  53. package/lib/ops/collation.d.ts +62 -0
  54. package/lib/ops/collation.js +228 -0
  55. package/lib/ops/commands.d.ts +9 -0
  56. package/lib/ops/commands.js +29 -0
  57. package/lib/ops/copies.d.ts +74 -0
  58. package/lib/ops/copies.js +135 -0
  59. package/lib/ops/draft.d.ts +59 -0
  60. package/lib/ops/draft.js +81 -0
  61. package/lib/ops/edits.d.ts +10 -0
  62. package/lib/ops/edits.js +82 -0
  63. package/lib/ops/features.d.ts +84 -0
  64. package/lib/ops/features.js +296 -0
  65. package/lib/ops/forget.d.ts +31 -0
  66. package/lib/ops/forget.js +110 -0
  67. package/lib/ops/immutable.d.ts +16 -0
  68. package/lib/ops/immutable.js +46 -0
  69. package/lib/ops/index.d.ts +13 -0
  70. package/lib/ops/index.js +7 -0
  71. package/lib/ops/versions.d.ts +33 -0
  72. package/lib/ops/versions.js +95 -0
  73. package/lib/readers/AtonParser.js +5 -7
  74. package/lib/readers/phillipsEroll.d.ts +3 -3
  75. package/lib/readers/phillipsEroll.js +4 -5
  76. package/lib/readers/spencerBar.d.ts +2 -2
  77. package/lib/readers/spencerBar.js +2 -3
  78. package/lib/readers/stanfordAton.d.ts +3 -3
  79. package/lib/readers/stanfordAton.js +2 -4
  80. package/lib/schema.json +11 -151
  81. package/lib/spec/context.json +2 -12
  82. package/lib/{ReproducingSystem.d.ts → systems/ReproducingSystem.d.ts} +3 -3
  83. package/lib/{TrackerBar.d.ts → systems/TrackerBar.d.ts} +48 -8
  84. package/lib/{TrackerBar.js → systems/TrackerBar.js} +11 -2
  85. package/lib/systems/index.d.ts +8 -2
  86. package/lib/systems/index.js +7 -1
  87. package/lib/systems/welte.d.ts +100 -0
  88. package/lib/systems/welte.js +119 -0
  89. package/lib/systems/welteLicensee/bar.d.ts +1 -1
  90. package/lib/systems/welteLicensee/bar.js +5 -2
  91. package/lib/systems/welteLicensee/system.d.ts +1 -1
  92. package/lib/systems/welteLicensee/system.js +1 -1
  93. package/lib/systems/welteT100/bar.d.ts +27 -1
  94. package/lib/systems/welteT100/bar.js +44 -2
  95. package/lib/systems/welteT100/system.d.ts +6 -39
  96. package/lib/systems/welteT100/system.js +19 -128
  97. package/lib/systems/welteT98/bar.d.ts +1 -1
  98. package/lib/systems/welteT98/bar.js +18 -2
  99. package/lib/systems/welteT98/system.d.ts +20 -38
  100. package/lib/systems/welteT98/system.js +20 -121
  101. package/lib/validate.d.ts +1 -1
  102. package/lib/{EditionView.d.ts → view/EditionView.d.ts} +38 -48
  103. package/lib/{EditionView.js → view/EditionView.js} +90 -96
  104. package/package.json +7 -2
  105. package/lib/Emulation.d.ts +0 -36
  106. package/lib/Emulation.js +0 -250
  107. package/lib/editionOps.d.ts +0 -260
  108. package/lib/editionOps.js +0 -1081
  109. package/lib/importJsonLd.js +0 -62
  110. /package/lib/{Agent.js → model/Agent.js} +0 -0
  111. /package/lib/{Assumption.js → model/Assumption.js} +0 -0
  112. /package/lib/{ConditionState.js → model/ConditionState.js} +0 -0
  113. /package/lib/{Edit.js → model/Edit.js} +0 -0
  114. /package/lib/{Edition.js → model/Edition.js} +0 -0
  115. /package/lib/{FeatureSource.js → model/FeatureSource.js} +0 -0
  116. /package/lib/{Perforator.js → model/Perforator.js} +0 -0
  117. /package/lib/{Quantity.d.ts → model/Quantity.d.ts} +0 -0
  118. /package/lib/{Quantity.js → model/Quantity.js} +0 -0
  119. /package/lib/{Symbol.js → model/Symbol.js} +0 -0
  120. /package/lib/{TrackCalibration.d.ts → model/TrackCalibration.d.ts} +0 -0
  121. /package/lib/{TrackCalibration.js → model/TrackCalibration.js} +0 -0
  122. /package/lib/{notes.d.ts → model/notes.d.ts} +0 -0
  123. /package/lib/{notes.js → model/notes.js} +0 -0
  124. /package/lib/{procedures.d.ts → model/procedures.d.ts} +0 -0
  125. /package/lib/{procedures.js → model/procedures.js} +0 -0
  126. /package/lib/{vocabulary.d.ts → model/vocabulary.d.ts} +0 -0
  127. /package/lib/{sorted.d.ts → shared/sorted.d.ts} +0 -0
  128. /package/lib/{sorted.js → shared/sorted.js} +0 -0
  129. /package/lib/{utils.d.ts → shared/utils.d.ts} +0 -0
  130. /package/lib/{utils.js → shared/utils.js} +0 -0
  131. /package/lib/{ReproducingSystem.js → systems/ReproducingSystem.js} +0 -0
@@ -0,0 +1,119 @@
1
+ import { clamp, geometryInMm, Grid, levelChanges, mezzoforteTravel, paperSeconds, ROWS_PER_MM, travelBetweenRails, } from "welte-mignon-emulator";
2
+ import { inCentimeters, mm, seconds } from "../model/Quantity.js";
3
+ import { partitionPoint } from "../shared/sorted.js";
4
+ import { velocityOf } from "./velocity.js";
5
+ /** Whether two sets of constants are the same, value for value. */
6
+ export const sameParameters = (a, b) => Object.keys(a).length === Object.keys(b).length
7
+ && Object.entries(a).every(([name, value]) => b[name] === value);
8
+ /** Paper the grid runs on past the last hole, so that a final pedal release completes. */
9
+ const RUN_OUT = mm(100);
10
+ const isNote = (event) => event.type === 'note';
11
+ const isExpression = (event) => event.type === 'expression';
12
+ const paperOf = (toOwnPaper) => ({
13
+ rowOf: place => place * toOwnPaper * ROWS_PER_MM,
14
+ placeOfRow: row => mm(row / (toOwnPaper * ROWS_PER_MM)),
15
+ paperOfRow: row => mm(row / ROWS_PER_MM)
16
+ });
17
+ /** When the spool brings a place on the roll to the tracker bar. */
18
+ export const secondsAt = (spool, place) => seconds(paperSeconds(spool, inCentimeters(place)));
19
+ const halfOf = (note, division) => note.vertical.from >= division ? 'treble' : 'bass';
20
+ /**
21
+ * One sample per row of the scan the constants were fitted on, from the
22
+ * beginning of the roll to a little past the last hole. The rows are
23
+ * equally spaced on the paper and not in time, which is what the
24
+ * emulator expects.
25
+ */
26
+ const gridOver = (events, spool, paper) => {
27
+ const last = mm(events.reduce((furthest, event) => Math.max(furthest, event.horizontal.to), 0));
28
+ const length = Math.ceil(paper.rowOf(last) + RUN_OUT * ROWS_PER_MM) + 1;
29
+ const times = new Float64Array(length).map((_, row) => secondsAt(spool, paper.paperOfRow(row)));
30
+ return new Grid(0, times);
31
+ };
32
+ const nuanceCurves = (grid, ports, samples, options, model, instrument) => {
33
+ const curveOf = (half) => {
34
+ const params = options.nuance[half];
35
+ const output = model.run({ grid, half, ports }, params);
36
+ const travel = travelBetweenRails(output, params);
37
+ const hook = clamp(mezzoforteTravel(params), 0.01, 0.99);
38
+ return {
39
+ ...samples,
40
+ name: half,
41
+ kind: 'dynamics',
42
+ instrument,
43
+ travel,
44
+ velocity: travel.map(value => velocityOf(value, hook, options.velocity))
45
+ };
46
+ };
47
+ return { bass: curveOf('bass'), treble: curveOf('treble') };
48
+ };
49
+ const performNotes = (events, grid, nuance, options, paper) => events
50
+ .filter(isNote)
51
+ .flatMap((note) => {
52
+ const curve = nuance[halfOf(note, options.division)];
53
+ const velocity = curve.velocity[grid.indexOfRow(paper.rowOf(note.horizontal.from))];
54
+ return [
55
+ { type: 'noteOn', performs: note, pitch: note.pitch, velocity, at: secondsAt(options.spool, note.horizontal.from) },
56
+ { type: 'noteOff', performs: note, pitch: note.pitch, velocity: 127, at: secondsAt(options.spool, note.horizontal.to) }
57
+ ];
58
+ });
59
+ /**
60
+ * The travel of one pedal as controller steps. Each step is attributed to
61
+ * the last perforation of that pedal the tracker bar has reached, which is
62
+ * the one whose reading it follows from.
63
+ */
64
+ const performPedal = (type, curve, grid, readings, mode) => {
65
+ if (readings.length === 0)
66
+ return [];
67
+ const ordered = readings.toSorted((a, b) => a.punch.rowOn - b.punch.rowOn);
68
+ const causeOf = (row) => ordered[Math.max(partitionPoint(ordered, reading => reading.punch.rowOn <= row) - 1, 0)].event;
69
+ return levelChanges(curve.travel, { mode })
70
+ .filter(change => change.index > 0)
71
+ .map(change => ({
72
+ type,
73
+ performs: causeOf(grid.rowAt(change.index)),
74
+ value: change.value,
75
+ at: seconds(curve.seconds[change.index])
76
+ }));
77
+ };
78
+ /**
79
+ * Plays the events on a Welte mechanism: notes at the velocity of their half's
80
+ * bellows, and the two pedals as the travel of theirs.
81
+ *
82
+ * The edition's tempo adjustment is left aside: it is stated as a paper
83
+ * speed, and what the spool holds constant is its rate of revolution, so
84
+ * the two are not the same quantity. The spool in the options sets the speed.
85
+ */
86
+ export const performWelte = (events, options, roll, mechanism) => {
87
+ const paper = paperOf(roll.toOwnPaper ?? 1);
88
+ const readings = events
89
+ .filter(isExpression)
90
+ .flatMap((event) => {
91
+ const punch = mechanism.punchOf(event, {
92
+ rowOn: paper.rowOf(event.horizontal.from),
93
+ rowOff: paper.rowOf(event.horizontal.to)
94
+ });
95
+ return punch ? [{ event, punch }] : [];
96
+ });
97
+ const grid = gridOver(events, options.spool, paper);
98
+ const geometry = geometryInMm(roll.punchDiameter ?? options.punchDiameter, options.trackerBore);
99
+ const ports = mechanism.portsOf(grid, readings.map(reading => reading.punch), geometry);
100
+ const samples = {
101
+ place: grid.seconds.map((_, row) => paper.placeOfRow(row)),
102
+ seconds: grid.seconds
103
+ };
104
+ const nuance = nuanceCurves(grid, ports, samples, options, mechanism.model, mechanism.instrument);
105
+ const travel = mechanism.runPedals({ grid, ports }, options.pedals);
106
+ const damper = { ...samples, name: 'damper', kind: 'pedal', travel: travel.damper };
107
+ const hammerRail = { ...samples, name: 'hammerRail', kind: 'pedal', travel: travel.hammerRail };
108
+ const readingsOf = (control) => readings.filter(reading => reading.punch.control === control);
109
+ return {
110
+ grid,
111
+ ports,
112
+ events: [
113
+ ...performNotes(events, grid, nuance, options, paper),
114
+ ...performPedal('damper', damper, grid, readingsOf('sustainPedal'), options.pedalMode),
115
+ ...performPedal('hammerRail', hammerRail, grid, readingsOf('hammerRail'), options.pedalMode)
116
+ ],
117
+ curves: [nuance.bass, nuance.treble, damper, hammerRail]
118
+ };
119
+ };
@@ -1,4 +1,4 @@
1
- import { TrackerBar } from "../../TrackerBar.js";
1
+ import { TrackerBar } from "../TrackerBar.js";
2
2
  /**
3
3
  * Welte-Mignon (Licensee), the American re-cut of the T-100 rolls on
4
4
  * a roll 11¼ inches wide with 98 positions at nine to the inch, cf.
@@ -1,5 +1,6 @@
1
- import { describeTrackerBar } from "../../TrackerBar.js";
2
- import { mm } from "../../Quantity.js";
1
+ import { describeTrackerBar } from "../TrackerBar.js";
2
+ import { mm } from "../../model/Quantity.js";
3
+ import { welteT100Accents, welteT100Operations } from "../welteT100/bar.js";
3
4
  /**
4
5
  * Welte-Mignon (Licensee), the American re-cut of the T-100 rolls on
5
6
  * a roll 11¼ inches wide with 98 positions at nine to the inch, cf.
@@ -15,6 +16,8 @@ export const welteLicensee = describeTrackerBar({
15
16
  width: mm(285.75),
16
17
  trackCount: 98,
17
18
  notes: { from: 9, to: 88, lowestPitch: 24 },
19
+ operations: welteT100Operations,
20
+ accents: welteT100Accents,
18
21
  expressions: new Map([
19
22
  [1, 'MezzoforteOff'],
20
23
  [2, 'MezzoforteOn'],
@@ -1,4 +1,4 @@
1
- import { ReproducingSystem } from "../../ReproducingSystem.js";
1
+ import { ReproducingSystem } from "../ReproducingSystem.js";
2
2
  import { type WelteT100Options } from "../welteT100/system.js";
3
3
  export type WelteLicenseeOptions = WelteT100Options;
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  import { CONSENSUS } from "welte-mignon-emulator/t100";
2
- import { track } from "../../Quantity.js";
2
+ import { track } from "../../model/Quantity.js";
3
3
  import { welteLicensee } from "./bar.js";
4
4
  import { defaultWelteT100Options, instrumentNameOf, nuanceOf, performAs } from "../welteT100/system.js";
5
5
  /**
@@ -1,9 +1,35 @@
1
- import { TrackerBar } from "../../TrackerBar.js";
1
+ import { Operation, TrackerBar } from "../TrackerBar.js";
2
2
  /**
3
3
  * The commands of the Welte-Mignon T-100, as its tracker bar reads them.
4
4
  */
5
5
  export declare const welteT100ExpressionTypes: readonly ['SustainPedalOn', 'SustainPedalOff', 'SoftPedalOn', 'SoftPedalOff', 'MezzoforteOff', 'MezzoforteOn', 'SlowCrescendoOn', 'SlowCrescendoOff', 'ForzandoOn', 'ForzandoOff', 'MotorOff', 'MotorOn', 'Rewind', 'ElectricCutOff'];
6
6
  export type WelteT100ExpressionType = typeof welteT100ExpressionTypes[number];
7
+ /**
8
+ * What each T-100 command operates, so that it can be set against the
9
+ * T-98's words for the same function.
10
+ *
11
+ * This cannot be read off the names. The T-100 latches a function on
12
+ * with one perforation and cancels it with a second, while the T-98
13
+ * holds it for as long as one perforation lasts (Hagmann pp. 89 f. and
14
+ * 100–103; Phillips p. 121), and Welte renamed two of the functions
15
+ * between the scales: the red `SlowCrescendo` is the green `Crescendo`,
16
+ * and the red's single `Forzando` valve answers to two green ones that
17
+ * name the end they pull towards. So the correspondence is stated here
18
+ * rather than derived, and only two types that operate one function can
19
+ * stand for each other.
20
+ *
21
+ * `MotorOn`/`MotorOff`, `Rewind` and `ElectricCutOff` are left out on
22
+ * purpose: the green scale has no word for any of them, its motor switch
23
+ * being an automatic mercury contact (Skala-Rolle 98 §12) and its rewind
24
+ * riding on the bass sforzando-piano line. A red command of those kinds
25
+ * has no counterpart and stays a plain deletion.
26
+ */
27
+ export declare const welteT100Operations: Readonly<Partial<Record<WelteT100ExpressionType, Operation>>>;
28
+ /**
29
+ * A single added accent on the T-100: a valve latched on and off again
30
+ * straight away.
31
+ */
32
+ export declare const welteT100Accents: readonly (readonly WelteT100ExpressionType[])[];
7
33
  /**
8
34
  * Welte-Mignon T-100 ("red Welte"), cf. Hagmann, pp. 75 and 178.
9
35
  *
@@ -1,5 +1,5 @@
1
- import { describeTrackerBar } from "../../TrackerBar.js";
2
- import { metersPerMinute, mm } from "../../Quantity.js";
1
+ import { describeTrackerBar } from "../TrackerBar.js";
2
+ import { metersPerMinute, mm } from "../../model/Quantity.js";
3
3
  /**
4
4
  * The commands of the Welte-Mignon T-100, as its tracker bar reads them.
5
5
  */
@@ -19,6 +19,46 @@ export const welteT100ExpressionTypes = [
19
19
  'Rewind',
20
20
  'ElectricCutOff'
21
21
  ];
22
+ /**
23
+ * What each T-100 command operates, so that it can be set against the
24
+ * T-98's words for the same function.
25
+ *
26
+ * This cannot be read off the names. The T-100 latches a function on
27
+ * with one perforation and cancels it with a second, while the T-98
28
+ * holds it for as long as one perforation lasts (Hagmann pp. 89 f. and
29
+ * 100–103; Phillips p. 121), and Welte renamed two of the functions
30
+ * between the scales: the red `SlowCrescendo` is the green `Crescendo`,
31
+ * and the red's single `Forzando` valve answers to two green ones that
32
+ * name the end they pull towards. So the correspondence is stated here
33
+ * rather than derived, and only two types that operate one function can
34
+ * stand for each other.
35
+ *
36
+ * `MotorOn`/`MotorOff`, `Rewind` and `ElectricCutOff` are left out on
37
+ * purpose: the green scale has no word for any of them, its motor switch
38
+ * being an automatic mercury contact (Skala-Rolle 98 §12) and its rewind
39
+ * riding on the bass sforzando-piano line. A red command of those kinds
40
+ * has no counterpart and stays a plain deletion.
41
+ */
42
+ export const welteT100Operations = {
43
+ MezzoforteOn: { operates: 'mezzoforte', spelling: 'on', sided: true },
44
+ MezzoforteOff: { operates: 'mezzoforte', spelling: 'off', sided: true },
45
+ SlowCrescendoOn: { operates: 'crescendo', spelling: 'on', sided: true },
46
+ SlowCrescendoOff: { operates: 'crescendo', spelling: 'off', sided: true },
47
+ ForzandoOn: { operates: 'sforzando', spelling: 'on', sided: true },
48
+ ForzandoOff: { operates: 'sforzando', spelling: 'off', sided: true },
49
+ SustainPedalOn: { operates: 'sustainPedal', spelling: 'on', sided: false },
50
+ SustainPedalOff: { operates: 'sustainPedal', spelling: 'off', sided: false },
51
+ SoftPedalOn: { operates: 'softPedal', spelling: 'on', sided: false },
52
+ SoftPedalOff: { operates: 'softPedal', spelling: 'off', sided: false }
53
+ };
54
+ /**
55
+ * A single added accent on the T-100: a valve latched on and off again
56
+ * straight away.
57
+ */
58
+ export const welteT100Accents = [
59
+ ['SlowCrescendoOn', 'SlowCrescendoOff'],
60
+ ['ForzandoOn', 'ForzandoOff']
61
+ ];
22
62
  /**
23
63
  * Welte-Mignon T-100 ("red Welte"), cf. Hagmann, pp. 75 and 178.
24
64
  *
@@ -34,6 +74,8 @@ export const welteT100 = describeTrackerBar({
34
74
  trackCount: 100,
35
75
  notes: { from: 11, to: 90, lowestPitch: 24 },
36
76
  paperSpeed: { value: metersPerMinute(3), unit: 'm/min' },
77
+ operations: welteT100Operations,
78
+ accents: welteT100Accents,
37
79
  expressions: new Map([
38
80
  [1, 'MezzoforteOff'],
39
81
  [2, 'MezzoforteOn'],
@@ -1,9 +1,9 @@
1
- import { Half, Parameters, PedalMode, Spool } from "welte-mignon-emulator";
1
+ import { Half, Parameters } from "welte-mignon-emulator";
2
2
  import { Instrument, RollNumber } from "welte-mignon-emulator/t100";
3
- import { NegotiatedEvent, Performance, ReproducingSystem, RollProperties } from "../../ReproducingSystem.js";
4
- import { Millimeters, Seconds, Track } from "../../Quantity.js";
5
- import { type VelocityMap } from "../velocity.js";
3
+ import { NegotiatedEvent, Performance, ReproducingSystem, RollProperties } from "../ReproducingSystem.js";
4
+ import { type WelteOptions } from "../welte.js";
6
5
  export type { VelocityMap } from "../velocity.js";
6
+ export { secondsAt } from "../welte.js";
7
7
  export type { Instrument } from "welte-mignon-emulator/t100";
8
8
  export type InstrumentName = 'consensus' | RollNumber;
9
9
  /**
@@ -34,48 +34,15 @@ export declare const pedalPresets: {
34
34
  export type PedalPreset = keyof typeof pedalPresets;
35
35
  /** The preset a set of pedal constants is, if it is one. */
36
36
  export declare const pedalPresetOf: (pedals: Parameters) => PedalPreset | undefined;
37
- export type WelteT100Options = {
38
- /**
39
- * The take-up spool, which sets the time axis: it is held at a constant
40
- * rate of revolution, so the paper runs faster as the spool fills.
41
- */
42
- spool: Spool;
43
- /** Constants of the nuancing mechanism, one set for each half of the keyboard: `nuanceOf` an instrument, or a set of one's own. */
44
- nuance: Record<Half, Parameters>;
45
- /** Constants of the two pedal actions, one of `pedalPresets` or a set of one's own. */
46
- pedals: Parameters;
47
- velocity: VelocityMap;
48
- /**
49
- * How the pedals go out: as a continuous controller carrying the travel
50
- * of the bellows, or thresholded to the two values a switching renderer
51
- * understands.
52
- */
53
- pedalMode: PedalMode;
54
- /** Diameter of the tracker-bar bore. */
55
- trackerBore: Millimeters;
56
- /** Punch diameter for an edition whose copies record none. */
57
- punchDiameter: Millimeters;
58
- /**
59
- * The track at which the keyboard is divided, so that notes from
60
- * here upwards follow the treble expression and the ones below it
61
- * the bass. Which side an expression perforation itself belongs to
62
- * is not decided here but read off the tracker bar.
63
- */
64
- division: Track;
65
- };
37
+ /** The T-100 takes the options every Welte system does; see `WelteOptions`. */
38
+ export type WelteT100Options = WelteOptions;
66
39
  export declare const defaultWelteT100Options: WelteT100Options;
67
- /** When the spool brings a place on the roll to the tracker bar. */
68
- export declare const secondsAt: (spool: Spool, place: Millimeters) => Seconds;
69
40
  /**
70
41
  * The mechanism, given a way of naming the instrument on the curves it
71
42
  * produces. The Licensee reads the same commands and is played by the same
72
43
  * valves, so it shares this; what it may not share is the name, since a
73
44
  * Licensee playback runs on constants fitted to Freiburg instruments and the
74
45
  * curve has to say so.
75
- *
76
- * The edition's tempo adjustment is left aside: it is stated as a paper
77
- * speed, and what the spool holds constant is its rate of revolution, so
78
- * the two are not the same quantity. The spool in the options sets the speed.
79
46
  */
80
47
  export declare const performAs: (instrumentOf: (nuance: Record<Half, Parameters>) => string) => (events: readonly NegotiatedEvent[], options: WelteT100Options, roll: RollProperties) => Performance;
81
48
  /**
@@ -1,9 +1,10 @@
1
- import { DEFAULT_PUNCH_MM, geometryInMm, Grid, levelChanges, paperSeconds, pedalBrushing, pedalDefaults, ROWS_PER_MM, TRACKER_BORE_MM, WELTE_SPOOL, } from "welte-mignon-emulator";
2
- import { aperturePorts, CONSENSUS, mezzoforteTravel, pneumaticModel, PRESETS, runPedals, travelBetweenRails, } from "welte-mignon-emulator/t100";
1
+ import { DEFAULT_PUNCH_MM, pedalBrushing, pedalDefaults, TRACKER_BORE_MM, WELTE_SPOOL, } from "welte-mignon-emulator";
2
+ import { aperturePorts, CONSENSUS, pneumaticModel, PRESETS, runPedals, } from "welte-mignon-emulator/t100";
3
3
  import { welteT100 } from "./bar.js";
4
- import { inCentimeters, mm, seconds, track } from "../../Quantity.js";
5
- import { partitionPoint } from "../../sorted.js";
6
- import { defaultVelocityMap, velocityOf } from "../velocity.js";
4
+ import { mm, track } from "../../model/Quantity.js";
5
+ import { defaultVelocityMap } from "../velocity.js";
6
+ import { performWelte, sameParameters } from "../welte.js";
7
+ export { secondsAt } from "../welte.js";
7
8
  /**
8
9
  * The instruments the emulator was fitted as: the consensus over the six
9
10
  * rolls with drawn nuance lines, and the setting that drew each of them,
@@ -13,8 +14,6 @@ import { defaultVelocityMap, velocityOf } from "../velocity.js";
13
14
  export const instruments = { consensus: CONSENSUS, ...PRESETS };
14
15
  /** The instruments by name, the consensus first. */
15
16
  export const instrumentNames = ['consensus', ...Object.keys(PRESETS)];
16
- const sameParameters = (a, b) => Object.keys(a).length === Object.keys(b).length
17
- && Object.entries(a).every(([name, value]) => b[name] === value);
18
17
  /** The nuancing constants of an instrument, one set for each half of the keyboard. */
19
18
  export const nuanceOf = (instrument) => ({ bass: instrument.bass, treble: instrument.treble });
20
19
  /** The instrument a pair of nuancing constants belongs to, if it is one. */
@@ -61,101 +60,12 @@ const CODES = {
61
60
  ElectricCutOff: ['electricCutoff', 'on']
62
61
  };
63
62
  const isWelteT100ExpressionType = (type) => Object.hasOwn(CODES, type);
64
- const codeOf = (expressionType) => isWelteT100ExpressionType(expressionType) ? CODES[expressionType] : undefined;
65
- /** Paper the grid runs on past the last hole, so that a final pedal release completes. */
66
- const RUN_OUT = mm(100);
67
- const isNote = (event) => event.type === 'note';
68
- const isExpression = (event) => event.type === 'expression';
69
- const paperOf = (toOwnPaper) => ({
70
- rowOf: place => place * toOwnPaper * ROWS_PER_MM,
71
- placeOfRow: row => mm(row / (toOwnPaper * ROWS_PER_MM)),
72
- paperOfRow: row => mm(row / ROWS_PER_MM)
73
- });
74
- /** When the spool brings a place on the roll to the tracker bar. */
75
- export const secondsAt = (spool, place) => seconds(paperSeconds(spool, inCentimeters(place)));
76
- const halfOf = (note, division) => note.vertical.from >= division ? 'treble' : 'bass';
77
- const readingOf = (paper) => (event) => {
78
- const code = codeOf(event.expressionType);
79
- if (!code)
63
+ /** The punch a T-100 expression is read as: its control, latched on or off. */
64
+ const punchOf = (event, rows) => {
65
+ if (!isWelteT100ExpressionType(event.expressionType))
80
66
  return undefined;
81
- const half = event.scope;
82
- const [control, action] = code;
83
- return {
84
- event,
85
- punch: {
86
- half,
87
- control,
88
- action,
89
- rowOn: paper.rowOf(event.horizontal.from),
90
- rowOff: paper.rowOf(event.horizontal.to)
91
- }
92
- };
93
- };
94
- const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
95
- /**
96
- * One sample per row of the scan the constants were fitted on, from the
97
- * beginning of the roll to a little past the last hole. The rows are
98
- * equally spaced on the paper and not in time, which is what the
99
- * emulator expects.
100
- */
101
- const gridOver = (events, spool, paper) => {
102
- const last = mm(events.reduce((furthest, event) => Math.max(furthest, event.horizontal.to), 0));
103
- const length = Math.ceil(paper.rowOf(last) + RUN_OUT * ROWS_PER_MM) + 1;
104
- const times = new Float64Array(length).map((_, row) => secondsAt(spool, paper.paperOfRow(row)));
105
- return new Grid(0, times);
106
- };
107
- const nuanceCurves = (grid, ports, samples, options, instrument) => {
108
- const curveOf = (half) => {
109
- const params = options.nuance[half];
110
- const output = pneumaticModel.run({ grid, half, ports }, params);
111
- const travel = travelBetweenRails(output, params);
112
- const hook = clamp(mezzoforteTravel(params), 0.01, 0.99);
113
- return {
114
- ...samples,
115
- name: half,
116
- kind: 'dynamics',
117
- instrument,
118
- travel,
119
- velocity: travel.map(value => velocityOf(value, hook, options.velocity))
120
- };
121
- };
122
- return { bass: curveOf('bass'), treble: curveOf('treble') };
123
- };
124
- const pedalCurves = (grid, ports, samples, options) => {
125
- const travel = runPedals({ grid, ports }, options.pedals);
126
- return {
127
- damper: { ...samples, name: 'damper', kind: 'pedal', travel: travel.damper },
128
- hammerRail: { ...samples, name: 'hammerRail', kind: 'pedal', travel: travel.hammerRail }
129
- };
130
- };
131
- const performNotes = (events, grid, nuance, options, paper) => events
132
- .filter(isNote)
133
- .flatMap((note) => {
134
- const curve = nuance[halfOf(note, options.division)];
135
- const velocity = curve.velocity[grid.indexOfRow(paper.rowOf(note.horizontal.from))];
136
- return [
137
- { type: 'noteOn', performs: note, pitch: note.pitch, velocity, at: secondsAt(options.spool, note.horizontal.from) },
138
- { type: 'noteOff', performs: note, pitch: note.pitch, velocity: 127, at: secondsAt(options.spool, note.horizontal.to) }
139
- ];
140
- });
141
- /**
142
- * The travel of one pedal as controller steps. Each step is attributed to
143
- * the last perforation of that pedal the tracker bar has reached, which is
144
- * the one whose reading it follows from.
145
- */
146
- const performPedal = (type, curve, grid, readings, mode) => {
147
- if (readings.length === 0)
148
- return [];
149
- const ordered = readings.toSorted((a, b) => a.punch.rowOn - b.punch.rowOn);
150
- const causeOf = (row) => ordered[Math.max(partitionPoint(ordered, reading => reading.punch.rowOn <= row) - 1, 0)].event;
151
- return levelChanges(curve.travel, { mode })
152
- .filter(change => change.index > 0)
153
- .map(change => ({
154
- type,
155
- performs: causeOf(grid.rowAt(change.index)),
156
- value: change.value,
157
- at: seconds(curve.seconds[change.index])
158
- }));
67
+ const [control, action] = CODES[event.expressionType];
68
+ return { half: event.scope, control, action, ...rows };
159
69
  };
160
70
  /**
161
71
  * The mechanism, given a way of naming the instrument on the curves it
@@ -163,35 +73,16 @@ const performPedal = (type, curve, grid, readings, mode) => {
163
73
  * valves, so it shares this; what it may not share is the name, since a
164
74
  * Licensee playback runs on constants fitted to Freiburg instruments and the
165
75
  * curve has to say so.
166
- *
167
- * The edition's tempo adjustment is left aside: it is stated as a paper
168
- * speed, and what the spool holds constant is its rate of revolution, so
169
- * the two are not the same quantity. The spool in the options sets the speed.
170
76
  */
171
77
  export const performAs = (instrumentOf) => (events, options, roll) => {
172
- const paper = paperOf(roll.toOwnPaper ?? 1);
173
- const readings = events
174
- .filter(isExpression)
175
- .map(readingOf(paper))
176
- .filter((reading) => reading !== undefined);
177
- const grid = gridOver(events, options.spool, paper);
178
- const geometry = geometryInMm(roll.punchDiameter ?? options.punchDiameter, options.trackerBore);
179
- const ports = aperturePorts(grid, readings.map(reading => reading.punch), geometry);
180
- const samples = {
181
- place: grid.seconds.map((_, row) => paper.placeOfRow(row)),
182
- seconds: grid.seconds
183
- };
184
- const nuance = nuanceCurves(grid, ports, samples, options, instrumentOf(options.nuance));
185
- const pedals = pedalCurves(grid, ports, samples, options);
186
- const readingsOf = (control) => readings.filter(reading => reading.punch.control === control);
187
- return {
188
- events: [
189
- ...performNotes(events, grid, nuance, options, paper),
190
- ...performPedal('damper', pedals.damper, grid, readingsOf('sustainPedal'), options.pedalMode),
191
- ...performPedal('hammerRail', pedals.hammerRail, grid, readingsOf('hammerRail'), options.pedalMode)
192
- ],
193
- curves: [nuance.bass, nuance.treble, pedals.damper, pedals.hammerRail]
194
- };
78
+ const { events: performed, curves } = performWelte(events, options, roll, {
79
+ punchOf,
80
+ portsOf: aperturePorts,
81
+ model: pneumaticModel,
82
+ runPedals,
83
+ instrument: instrumentOf(options.nuance)
84
+ });
85
+ return { events: performed, curves };
195
86
  };
196
87
  /**
197
88
  * The red Welte, as welte-mignon-emulator models it: the take-up spool sets
@@ -1,4 +1,4 @@
1
- import { TrackerBar } from "../../TrackerBar.js";
1
+ import { TrackerBar } from "../TrackerBar.js";
2
2
  /**
3
3
  * The commands of the Welte-Mignon T-98, as its tracker bar reads them.
4
4
  *
@@ -1,5 +1,5 @@
1
- import { describeTrackerBar } from "../../TrackerBar.js";
2
- import { metersPerMinute, mm } from "../../Quantity.js";
1
+ import { describeTrackerBar } from "../TrackerBar.js";
2
+ import { metersPerMinute, mm } from "../../model/Quantity.js";
3
3
  /**
4
4
  * The commands of the Welte-Mignon T-98, as its tracker bar reads them.
5
5
  *
@@ -17,6 +17,19 @@ export const welteT98ExpressionTypes = [
17
17
  'SustainPedal',
18
18
  'SoftPedal'
19
19
  ];
20
+ /**
21
+ * What each T-98 command operates. Every one of them holds its function
22
+ * for as long as its perforation lasts; see `welteT100Operations` for
23
+ * how the functions answer to the T-100's.
24
+ */
25
+ const welteT98Operations = {
26
+ Mezzoforte: { operates: 'mezzoforte', spelling: 'held', sided: true },
27
+ Crescendo: { operates: 'crescendo', spelling: 'held', sided: true },
28
+ SforzandoForte: { operates: 'sforzando', spelling: 'held', sided: true },
29
+ SforzandoPiano: { operates: 'sforzando', spelling: 'held', sided: true },
30
+ SustainPedal: { operates: 'sustainPedal', spelling: 'held', sided: false },
31
+ SoftPedal: { operates: 'softPedal', spelling: 'held', sided: false }
32
+ };
20
33
  /**
21
34
  * Welte-Mignon T-98 ("green Welte"), cf. Hagmann, Anhang 11 (p. 179)
22
35
  * and Phillips, p. 121.
@@ -56,6 +69,9 @@ export const welteT98 = describeTrackerBar({
56
69
  notes: { from: 6, to: 93, lowestPitch: 21 },
57
70
  paperSpeed: { value: metersPerMinute(2.2), unit: 'm/min' },
58
71
  rewindTrack: 1,
72
+ operations: welteT98Operations,
73
+ /** A single added accent on the T-98 is one held perforation. */
74
+ accents: [['Crescendo'], ['SforzandoForte']],
59
75
  /**
60
76
  * The T-98 gives the rewind no line of its own — Welte's Forzando P line
61
77
  * doubles as it — so a perforation there is the rewind only when it is far
@@ -1,9 +1,10 @@
1
- import { Half, Parameters, PedalMode, Spool } from "welte-mignon-emulator";
1
+ import { Half, Parameters } from "welte-mignon-emulator";
2
2
  import { instrumentT98Of, labelOf, nuanceOf, WelteT98InstrumentName } from "welte-mignon-emulator/t98";
3
- import { ReproducingSystem } from "../../ReproducingSystem.js";
4
- import { Millimeters, Seconds, Track } from "../../Quantity.js";
5
- import { type VelocityMap } from "../velocity.js";
3
+ import { ReproducingSystem } from "../ReproducingSystem.js";
4
+ import { Millimeters } from "../../model/Quantity.js";
5
+ import { type WelteOptions } from "../welte.js";
6
6
  export type { VelocityMap } from "../velocity.js";
7
+ export { secondsAt } from "../welte.js";
7
8
  export type { WelteT98Instrument, WelteT98InstrumentName } from "welte-mignon-emulator/t98";
8
9
  /**
9
10
  * The instruments the emulator offers for the green Welte, in three groups that
@@ -54,27 +55,22 @@ export declare const pedalPresets: {
54
55
  };
55
56
  export type PedalPreset = keyof typeof pedalPresets;
56
57
  export declare const pedalPresetOf: (pedals: Parameters) => PedalPreset | undefined;
57
- export type WelteT98Options = {
58
- /**
59
- * The take-up spool, which sets the time axis. No source states a T-98
60
- * spool geometry, so the default keeps the red circumference and layer and
61
- * sets the revolution to make the initial paper speed 220 cm/min. It is
62
- * better varied than trusted: it scales every conductance by k and every
63
- * time constant by 1/k, and touches nothing dimensionless.
64
- */
65
- spool: Spool;
66
- /** Constants of the nuancing mechanism, one set for each half of the keyboard. */
67
- nuance: Record<Half, Parameters>;
68
- /** Which instrument those constants are, so that a curve can say so. */
58
+ /**
59
+ * The options every Welte system takes (see `WelteOptions`), with what the
60
+ * T-98 adds. No source states a T-98 spool geometry, so the default keeps the
61
+ * red circumference and layer and sets the revolution to make the initial
62
+ * paper speed 220 cm/min; it is better varied than trusted, since it scales
63
+ * every conductance by k and every time constant by 1/k and touches nothing
64
+ * dimensionless. No source gives the T-98's tracker bore either, and the red
65
+ * figure is carried over. Welte puts the division between f♯ and g
66
+ * (Betriebsanleitung p. 7), which on this bar is track 52; PlaySK's green
67
+ * configuration independently gives the last bass note as MIDI 66. A green
68
+ * roll re-cut from a Mignon master uses only the middle 80 of the 88 note
69
+ * positions, so the division falls inside the used compass either way.
70
+ */
71
+ export type WelteT98Options = WelteOptions & {
72
+ /** Which instrument the nuancing constants are, so that a curve can say so. */
69
73
  instrument: WelteT98InstrumentName;
70
- /** Constants of the two pedal actions, one of `pedalPresets` or a set of one's own. */
71
- pedals: Parameters;
72
- velocity: VelocityMap;
73
- pedalMode: PedalMode;
74
- /** Diameter of the tracker-bar bore. No source gives the T-98's; the red figure is carried over. */
75
- trackerBore: Millimeters;
76
- /** Punch diameter for an edition whose copies record none. */
77
- punchDiameter: Millimeters;
78
74
  /**
79
75
  * Chained punches whose gap is shorter than this are one perforation. A held
80
76
  * T-98 command is punched as a chain of round holes on a 2.66 mm grid with
@@ -83,18 +79,6 @@ export type WelteT98Options = {
83
79
  * same port only if the chain is merged.
84
80
  */
85
81
  chainGap: Millimeters;
86
- /**
87
- * The track at which the keyboard is divided, so that notes from here
88
- * upwards follow the treble expression and the ones below it the bass.
89
- * Welte puts the division between f♯ and g (Betriebsanleitung p. 7), which
90
- * on this bar is track 52; PlaySK's green configuration independently gives
91
- * the last bass note as MIDI 66. Which side an expression perforation
92
- * belongs to is not decided here but read off the tracker bar.
93
- *
94
- * A green roll re-cut from a Mignon master uses only the middle 80 of the 88
95
- * note positions, so the division falls inside the used compass either way.
96
- */
97
- division: Track;
98
82
  /**
99
83
  * What a long perforation on the bass sforzando-piano line does. It is the
100
84
  * same valve and the same hole as the dynamic (Skala-Rolle §10), so it
@@ -108,8 +92,6 @@ export type WelteT98Options = {
108
92
  rewind: 'stop' | 'ignore';
109
93
  };
110
94
  export declare const defaultWelteT98Options: WelteT98Options;
111
- /** When the spool brings a place on the roll to the tracker bar. */
112
- export declare const secondsAt: (spool: Spool, place: Millimeters) => Seconds;
113
95
  /**
114
96
  * The green Welte, as welte-mignon-emulator models it. Everything downstream of
115
97
  * the relay is the T-100's, on Hagmann's authority that the nuancing unit is