linked-rolls 0.57.0 → 0.59.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 (130) hide show
  1. package/README.md +54 -367
  2. package/lib/{constraints.d.ts → analysis/constraints.d.ts} +1 -1
  3. package/lib/{constraints.js → analysis/constraints.js} +8 -7
  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} +2 -2
  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} +3 -3
  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} +3 -3
  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.js → io/migrate.js} +59 -55
  35. package/lib/{Agent.d.ts → model/Agent.d.ts} +1 -1
  36. package/lib/{Assumption.d.ts → model/Assumption.d.ts} +1 -1
  37. package/lib/{Edit.d.ts → model/Edit.d.ts} +1 -1
  38. package/lib/{Edition.d.ts → model/Edition.d.ts} +2 -2
  39. package/lib/{Feature.d.ts → model/Feature.d.ts} +1 -1
  40. package/lib/{FeatureSource.d.ts → model/FeatureSource.d.ts} +1 -1
  41. package/lib/{Perforator.d.ts → model/Perforator.d.ts} +1 -1
  42. package/lib/{RollCopy.d.ts → model/RollCopy.d.ts} +3 -10
  43. package/lib/{RollCopy.js → model/RollCopy.js} +3 -5
  44. package/lib/{Symbol.d.ts → model/Symbol.d.ts} +1 -1
  45. package/lib/{Version.d.ts → model/Version.d.ts} +10 -2
  46. package/lib/{Version.js → model/Version.js} +24 -1
  47. package/lib/{vocabulary.js → model/vocabulary.js} +2 -2
  48. package/lib/ops/beliefs.d.ts +9 -0
  49. package/lib/ops/beliefs.js +31 -0
  50. package/lib/ops/collation.d.ts +62 -0
  51. package/lib/ops/collation.js +228 -0
  52. package/lib/ops/commands.d.ts +9 -0
  53. package/lib/ops/commands.js +29 -0
  54. package/lib/ops/copies.d.ts +74 -0
  55. package/lib/ops/copies.js +135 -0
  56. package/lib/ops/draft.d.ts +59 -0
  57. package/lib/ops/draft.js +81 -0
  58. package/lib/ops/edits.d.ts +10 -0
  59. package/lib/ops/edits.js +82 -0
  60. package/lib/ops/features.d.ts +84 -0
  61. package/lib/ops/features.js +296 -0
  62. package/lib/ops/forget.d.ts +31 -0
  63. package/lib/ops/forget.js +110 -0
  64. package/lib/ops/immutable.d.ts +16 -0
  65. package/lib/ops/immutable.js +46 -0
  66. package/lib/ops/index.d.ts +13 -0
  67. package/lib/ops/index.js +7 -0
  68. package/lib/ops/versions.d.ts +33 -0
  69. package/lib/ops/versions.js +95 -0
  70. package/lib/readers/AtonParser.js +5 -7
  71. package/lib/readers/phillipsEroll.d.ts +3 -3
  72. package/lib/readers/phillipsEroll.js +4 -5
  73. package/lib/readers/spencerBar.d.ts +2 -2
  74. package/lib/readers/spencerBar.js +2 -3
  75. package/lib/readers/stanfordAton.d.ts +3 -3
  76. package/lib/readers/stanfordAton.js +2 -3
  77. package/lib/schema.json +0 -13
  78. package/lib/{ReproducingSystem.d.ts → systems/ReproducingSystem.d.ts} +3 -3
  79. package/lib/{TrackerBar.d.ts → systems/TrackerBar.d.ts} +48 -8
  80. package/lib/{TrackerBar.js → systems/TrackerBar.js} +11 -2
  81. package/lib/systems/index.d.ts +8 -2
  82. package/lib/systems/index.js +7 -1
  83. package/lib/systems/welte.d.ts +100 -0
  84. package/lib/systems/welte.js +119 -0
  85. package/lib/systems/welteLicensee/bar.d.ts +1 -1
  86. package/lib/systems/welteLicensee/bar.js +5 -2
  87. package/lib/systems/welteLicensee/system.d.ts +1 -1
  88. package/lib/systems/welteLicensee/system.js +1 -1
  89. package/lib/systems/welteT100/bar.d.ts +27 -1
  90. package/lib/systems/welteT100/bar.js +44 -2
  91. package/lib/systems/welteT100/system.d.ts +6 -39
  92. package/lib/systems/welteT100/system.js +19 -128
  93. package/lib/systems/welteT98/bar.d.ts +1 -1
  94. package/lib/systems/welteT98/bar.js +18 -2
  95. package/lib/systems/welteT98/system.d.ts +20 -38
  96. package/lib/systems/welteT98/system.js +20 -121
  97. package/lib/validate.d.ts +1 -1
  98. package/lib/{EditionView.d.ts → view/EditionView.d.ts} +17 -41
  99. package/lib/{EditionView.js → view/EditionView.js} +26 -75
  100. package/package.json +7 -2
  101. package/lib/Emulation.d.ts +0 -36
  102. package/lib/Emulation.js +0 -250
  103. package/lib/editionOps.d.ts +0 -260
  104. package/lib/editionOps.js +0 -1079
  105. package/lib/importJsonLd.js +0 -57
  106. /package/lib/{migrate.d.ts → io/migrate.d.ts} +0 -0
  107. /package/lib/{Agent.js → model/Agent.js} +0 -0
  108. /package/lib/{Assumption.js → model/Assumption.js} +0 -0
  109. /package/lib/{ConditionState.d.ts → model/ConditionState.d.ts} +0 -0
  110. /package/lib/{ConditionState.js → model/ConditionState.js} +0 -0
  111. /package/lib/{Edit.js → model/Edit.js} +0 -0
  112. /package/lib/{Edition.js → model/Edition.js} +0 -0
  113. /package/lib/{Feature.js → model/Feature.js} +0 -0
  114. /package/lib/{FeatureSource.js → model/FeatureSource.js} +0 -0
  115. /package/lib/{Perforator.js → model/Perforator.js} +0 -0
  116. /package/lib/{Quantity.d.ts → model/Quantity.d.ts} +0 -0
  117. /package/lib/{Quantity.js → model/Quantity.js} +0 -0
  118. /package/lib/{Symbol.js → model/Symbol.js} +0 -0
  119. /package/lib/{TrackCalibration.d.ts → model/TrackCalibration.d.ts} +0 -0
  120. /package/lib/{TrackCalibration.js → model/TrackCalibration.js} +0 -0
  121. /package/lib/{notes.d.ts → model/notes.d.ts} +0 -0
  122. /package/lib/{notes.js → model/notes.js} +0 -0
  123. /package/lib/{procedures.d.ts → model/procedures.d.ts} +0 -0
  124. /package/lib/{procedures.js → model/procedures.js} +0 -0
  125. /package/lib/{vocabulary.d.ts → model/vocabulary.d.ts} +0 -0
  126. /package/lib/{sorted.d.ts → shared/sorted.d.ts} +0 -0
  127. /package/lib/{sorted.js → shared/sorted.js} +0 -0
  128. /package/lib/{utils.d.ts → shared/utils.d.ts} +0 -0
  129. /package/lib/{utils.js → shared/utils.js} +0 -0
  130. /package/lib/{ReproducingSystem.js → systems/ReproducingSystem.js} +0 -0
@@ -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
@@ -1,9 +1,10 @@
1
- import { geometryInMm, Grid, levelChanges, mezzoforteTravel, paperSeconds, pedalBrushing, pedalDefaults, ROWS_PER_MM, travelBetweenRails, WELTE_T98_SPOOL, } from "welte-mignon-emulator";
1
+ import { pedalBrushing, pedalDefaults, ROWS_PER_MM, WELTE_T98_SPOOL, } from "welte-mignon-emulator";
2
2
  import { aperturePorts, CHAIN_GAP_MM, DERIVED, GENUINE, instrumentT98Of, labelOf, nuanceOf, PUNCH_T98_MM, pneumaticT98Model, rewindAt, runPedals, sforzandoPianoLift, STARTING_VALUES, TRACKER_BORE_T98_MM, } from "welte-mignon-emulator/t98";
3
3
  import { welteT98 } 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, seconds, 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 offers for the green Welte, in three groups that
9
10
  * answer three different questions and must not be listed as one.
@@ -37,8 +38,6 @@ export const instrumentNames = [
37
38
  ...Object.keys(DERIVED).map(derived => ({ derived })),
38
39
  { unfitted: 'starting-values' }
39
40
  ];
40
- const sameParameters = (a, b) => Object.keys(a).length === Object.keys(b).length
41
- && Object.entries(a).every(([name, value]) => b[name] === value);
42
41
  /** The instrument a pair of nuancing constants belongs to, if it is one. */
43
42
  export const instrumentNameOf = (nuance) => instrumentNames.find(name => {
44
43
  const instrument = instrumentT98Of(name);
@@ -86,98 +85,10 @@ const CODES = {
86
85
  SoftPedal: 'hammerRail'
87
86
  };
88
87
  const isWelteT98ExpressionType = (type) => Object.hasOwn(CODES, type);
89
- const codeOf = (expressionType) => isWelteT98ExpressionType(expressionType) ? CODES[expressionType] : undefined;
90
- /** Paper the grid runs on past the last hole, so that a final pedal release completes. */
91
- const RUN_OUT = mm(100);
92
- const isNote = (event) => event.type === 'note';
93
- const isExpression = (event) => event.type === 'expression';
94
- const paperOf = (toOwnPaper) => ({
95
- rowOf: place => place * toOwnPaper * ROWS_PER_MM,
96
- placeOfRow: row => mm(row / (toOwnPaper * ROWS_PER_MM)),
97
- paperOfRow: row => mm(row / ROWS_PER_MM)
98
- });
99
- /** When the spool brings a place on the roll to the tracker bar. */
100
- export const secondsAt = (spool, place) => seconds(paperSeconds(spool, inCentimeters(place)));
101
- const halfOf = (note, division) => note.vertical.from >= division ? 'treble' : 'bass';
102
- const readingOf = (paper) => (event) => {
103
- const control = codeOf(event.expressionType);
104
- if (!control)
105
- return undefined;
106
- const half = event.scope;
107
- return {
108
- event,
109
- punch: {
110
- half,
111
- control,
112
- rowOn: paper.rowOf(event.horizontal.from),
113
- rowOff: paper.rowOf(event.horizontal.to)
114
- }
115
- };
116
- };
117
- const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
118
- /**
119
- * One sample per row of the scan the constants were fitted on, from the
120
- * beginning of the roll to a little past the last hole.
121
- */
122
- const gridOver = (events, spool, paper) => {
123
- const last = mm(events.reduce((furthest, event) => Math.max(furthest, event.horizontal.to), 0));
124
- const length = Math.ceil(paper.rowOf(last) + RUN_OUT * ROWS_PER_MM) + 1;
125
- const times = new Float64Array(length).map((_, row) => secondsAt(spool, paper.paperOfRow(row)));
126
- return new Grid(0, times);
127
- };
128
- const nuanceCurves = (grid, ports, samples, options) => {
129
- const instrument = `Welte-Mignon T-98, ${labelOf(options.instrument)}`;
130
- const curveOf = (half) => {
131
- const params = options.nuance[half];
132
- const output = pneumaticT98Model.run({ grid, half, ports }, params);
133
- const travel = travelBetweenRails(output, params);
134
- const hook = clamp(mezzoforteTravel(params), 0.01, 0.99);
135
- return {
136
- ...samples,
137
- name: half,
138
- kind: 'dynamics',
139
- instrument,
140
- travel,
141
- velocity: travel.map(value => velocityOf(value, hook, options.velocity))
142
- };
143
- };
144
- return { bass: curveOf('bass'), treble: curveOf('treble') };
145
- };
146
- const pedalCurves = (grid, ports, samples, options) => {
147
- const travel = runPedals({ grid, ports }, options.pedals);
148
- return {
149
- damper: { ...samples, name: 'damper', kind: 'pedal', travel: travel.damper },
150
- hammerRail: { ...samples, name: 'hammerRail', kind: 'pedal', travel: travel.hammerRail }
151
- };
152
- };
153
- const performNotes = (events, grid, nuance, options, paper) => events
154
- .filter(isNote)
155
- .flatMap((note) => {
156
- const curve = nuance[halfOf(note, options.division)];
157
- const velocity = curve.velocity[grid.indexOfRow(paper.rowOf(note.horizontal.from))];
158
- return [
159
- { type: 'noteOn', performs: note, pitch: note.pitch, velocity, at: secondsAt(options.spool, note.horizontal.from) },
160
- { type: 'noteOff', performs: note, pitch: note.pitch, velocity: 127, at: secondsAt(options.spool, note.horizontal.to) }
161
- ];
162
- });
163
- /**
164
- * The travel of one pedal as controller steps, each attributed to the last
165
- * perforation of that pedal the tracker bar has reached.
166
- */
167
- const performPedal = (type, curve, grid, readings, mode) => {
168
- if (readings.length === 0)
169
- return [];
170
- const ordered = readings.toSorted((a, b) => a.punch.rowOn - b.punch.rowOn);
171
- const causeOf = (row) => ordered[Math.max(partitionPoint(ordered, reading => reading.punch.rowOn <= row) - 1, 0)].event;
172
- return levelChanges(curve.travel, { mode })
173
- .filter(change => change.index > 0)
174
- .map(change => ({
175
- type,
176
- performs: causeOf(grid.rowAt(change.index)),
177
- value: change.value,
178
- at: seconds(curve.seconds[change.index])
179
- }));
180
- };
88
+ /** The punch a T-98 expression is read as: its control, held for as long as it runs. */
89
+ const punchOf = (event, rows) => isWelteT98ExpressionType(event.expressionType)
90
+ ? { half: event.scope, control: CODES[event.expressionType], ...rows }
91
+ : undefined;
181
92
  /**
182
93
  * The row at which the Abstellbalg trips and the roll goes back, or the end of
183
94
  * the grid where it never does. The shut-off hangs on the bass sforzando-piano
@@ -199,36 +110,24 @@ const cutCurve = (curve, rows) => ({
199
110
  ...(curve.kind === 'dynamics' ? { velocity: curve.velocity.slice(0, rows) } : {})
200
111
  });
201
112
  /**
202
- * The edition's tempo adjustment is left aside, as on the T-100: it is stated as
203
- * a paper speed, and what the spool holds constant is its rate of revolution.
113
+ * The shared Welte performance, cut off where the rewind takes hold unless
114
+ * the options ignore it.
204
115
  */
205
116
  const perform = (events, options, roll) => {
206
- const paper = paperOf(roll.toOwnPaper ?? 1);
207
- const readings = events
208
- .filter(isExpression)
209
- .map(readingOf(paper))
210
- .filter((reading) => reading !== undefined);
211
- const grid = gridOver(events, options.spool, paper);
212
- const geometry = geometryInMm(roll.punchDiameter ?? options.punchDiameter, options.trackerBore);
213
117
  const gap = options.chainGap * ROWS_PER_MM;
214
- const ports = aperturePorts(grid, readings.map(reading => reading.punch), geometry, gap);
215
- const samples = {
216
- place: grid.seconds.map((_, row) => paper.placeOfRow(row)),
217
- seconds: grid.seconds
218
- };
219
- const nuance = nuanceCurves(grid, ports, samples, options);
220
- const pedals = pedalCurves(grid, ports, samples, options);
221
- const readingsOf = (control) => readings.filter(reading => reading.punch.control === control);
118
+ const { grid, ports, events: performed, curves } = performWelte(events, options, roll, {
119
+ punchOf,
120
+ portsOf: (grid, punches, geometry) => aperturePorts(grid, punches, geometry, gap),
121
+ model: pneumaticT98Model,
122
+ runPedals,
123
+ instrument: `Welte-Mignon T-98, ${labelOf(options.instrument)}`
124
+ });
222
125
  const stops = rewindRow(grid, ports, options);
223
126
  const until = seconds(grid.seconds[Math.min(stops, grid.length - 1)]);
224
127
  const rows = Math.min(stops + 1, grid.length);
225
128
  return {
226
- events: truncated([
227
- ...performNotes(events, grid, nuance, options, paper),
228
- ...performPedal('damper', pedals.damper, grid, readingsOf('sustainPedal'), options.pedalMode),
229
- ...performPedal('hammerRail', pedals.hammerRail, grid, readingsOf('hammerRail'), options.pedalMode)
230
- ], until),
231
- curves: [nuance.bass, nuance.treble, pedals.damper, pedals.hammerRail].map(curve => cutCurve(curve, rows))
129
+ events: truncated(performed, until),
130
+ curves: curves.map(curve => cutCurve(curve, rows))
232
131
  };
233
132
  };
234
133
  /**
package/lib/validate.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ErrorObject } from "ajv";
2
- import { Edition } from "./Edition.js";
2
+ import { Edition } from "./model/Edition.js";
3
3
  /**
4
4
  * Holds a document against the edition schema. `errors` says what the
5
5
  * last call found wrong, and is empty where it found nothing.