linked-rolls 0.19.1 → 0.21.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.
- package/README.md +31 -6
- package/lib/Collation.d.ts +1 -3
- package/lib/Collation.js +9 -11
- package/lib/Edition.d.ts +1 -9
- package/lib/EditionView.d.ts +54 -6
- package/lib/EditionView.js +93 -20
- package/lib/Emulation.js +5 -4
- package/lib/ReproducingSystem.d.ts +38 -2
- package/lib/RollCopy.d.ts +25 -2
- package/lib/RollCopy.js +30 -4
- package/lib/TrackerBar.d.ts +57 -2
- package/lib/TrackerBar.js +35 -17
- package/lib/Version.d.ts +49 -1
- package/lib/asJsonLd.js +23 -8
- package/lib/constraints.d.ts +8 -19
- package/lib/constraints.js +71 -15
- package/lib/context.d.ts +10 -1
- package/lib/context.js +10 -1
- package/lib/editionOps.d.ts +3 -1
- package/lib/editionOps.js +77 -15
- package/lib/importJsonLd.js +4 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/migrate.js +85 -11
- package/lib/readers/phillipsEroll.d.ts +4 -6
- package/lib/readers/phillipsEroll.js +6 -7
- package/lib/readers/spencerBar.d.ts +4 -5
- package/lib/readers/spencerBar.js +7 -8
- package/lib/readers/stanfordAton.d.ts +4 -7
- package/lib/readers/stanfordAton.js +4 -5
- package/lib/reservations.d.ts +1 -1
- package/lib/reservations.js +24 -2
- package/lib/schema.json +27 -5
- package/lib/spec/context.json +1 -0
- package/lib/spec/welte-licensee.context.json +10 -0
- package/lib/spec/welte-t98.context.json +10 -0
- package/lib/substitution.d.ts +73 -0
- package/lib/substitution.js +175 -0
- package/lib/systems/velocity.d.ts +34 -0
- package/lib/systems/velocity.js +36 -0
- package/lib/systems/welteLicensee/system.d.ts +46 -0
- package/lib/systems/welteLicensee/system.js +60 -0
- package/lib/systems/welteT100/system.d.ts +19 -16
- package/lib/systems/welteT100/system.js +42 -47
- package/lib/systems/welteT98/bar.d.ts +15 -2
- package/lib/systems/welteT98/bar.js +31 -3
- package/lib/systems/welteT98/system.d.ts +119 -0
- package/lib/systems/welteT98/system.js +245 -0
- package/package.json +12 -4
|
@@ -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-98, as its tracker bar reads them.
|
|
5
5
|
*
|
|
@@ -32,8 +32,21 @@ export const welteT98ExpressionTypes = [
|
|
|
32
32
|
*
|
|
33
33
|
* Measured against Julian Dyer's scan of the T-98 copy of roll 225
|
|
34
34
|
* (Grünfeld, Träumerei): 98 columns 2.818 mm apart on paper 285.6 mm
|
|
35
|
-
* wide, notes on the middle 80 columns, the sustain valve punched
|
|
36
|
-
* times where the T-100 copy latches its sustain on
|
|
35
|
+
* wide, notes on the middle 80 columns, the sustain valve punched 52
|
|
36
|
+
* times in the music where the T-100 copy latches its sustain on 52
|
|
37
|
+
* times. A 53rd perforation on that track belongs to the test pattern
|
|
38
|
+
* after the rewind.
|
|
39
|
+
*
|
|
40
|
+
* The paper speed is the Deutsches Museum's figure for its Welte grün /
|
|
41
|
+
* T 98 rolls, 220 cm/min. Phillips gives 7 ft/min = 2.134 m/min (p. 121);
|
|
42
|
+
* midi2exp's and PlaySK's green defaults of 72.2 and 72.27, read as the
|
|
43
|
+
* Stanford convention of feet per minute times ten, are 2.201 m/min, which
|
|
44
|
+
* is the museum's figure; and the two copies of roll 225 last the same
|
|
45
|
+
* time if the green starts at about 7.30 ft/min. Welte's own booklets give
|
|
46
|
+
* no speed, only that the roll must run from the first "A" of the chromatic
|
|
47
|
+
* scale to the cross-line bearing the dial number in half a minute
|
|
48
|
+
* (Skala-Rolle 98 §1b). This is documentation; what sets the emulator's
|
|
49
|
+
* time axis is the spool.
|
|
37
50
|
*/
|
|
38
51
|
export const welteT98 = describeTrackerBar({
|
|
39
52
|
id: 'welte-green',
|
|
@@ -41,7 +54,22 @@ export const welteT98 = describeTrackerBar({
|
|
|
41
54
|
width: mm(286),
|
|
42
55
|
trackCount: 98,
|
|
43
56
|
notes: { from: 6, to: 93, lowestPitch: 21 },
|
|
57
|
+
paperSpeed: { value: metersPerMinute(2.2), unit: 'm/min' },
|
|
44
58
|
rewindTrack: 1,
|
|
59
|
+
/**
|
|
60
|
+
* The T-98 gives the rewind no line of its own — Welte's Forzando P line
|
|
61
|
+
* doubles as it — so a perforation there is the rewind only when it is far
|
|
62
|
+
* longer than the command that line usually carries. On the Monteurscala
|
|
63
|
+
* the rewind is a single continuous 103.8 mm perforation (Kontrolle 10, 40
|
|
64
|
+
* punches with gaps of 0.51–0.93 mm), where a commanded sforzando-piano
|
|
65
|
+
* runs a couple of millimetres — a median of 0.06 s against the rewind's
|
|
66
|
+
* 10.2 s on WM 184. Dyer's scan of the green 225 says the same a third time:
|
|
67
|
+
* of the 30 perforations its track 1 carries, exactly one reaches 40 mm and
|
|
68
|
+
* it is 386 mm long. So the threshold is not a fitted number and does not
|
|
69
|
+
* need to be — every measurement of it so far leaves an order of magnitude
|
|
70
|
+
* either side.
|
|
71
|
+
*/
|
|
72
|
+
rewindHold: mm(40),
|
|
45
73
|
expressions: new Map([
|
|
46
74
|
[1, 'SforzandoPiano'],
|
|
47
75
|
[2, 'Mezzoforte'],
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Half, Parameters, PedalMode, Spool } from "welte-mignon-emulator";
|
|
2
|
+
import { instrumentT98Of, labelOf, nuanceOf, WelteT98InstrumentName } from "welte-mignon-emulator/t98";
|
|
3
|
+
import { ReproducingSystem } from "../../ReproducingSystem";
|
|
4
|
+
import { Millimeters, Seconds, Track } from "../../Quantity";
|
|
5
|
+
import { type VelocityMap } from "../velocity";
|
|
6
|
+
export type { VelocityMap } from "../velocity";
|
|
7
|
+
export type { WelteT98Instrument, WelteT98InstrumentName } from "welte-mignon-emulator/t98";
|
|
8
|
+
/**
|
|
9
|
+
* The instruments the emulator offers for the green Welte, in three groups that
|
|
10
|
+
* answer three different questions and must not be listed as one.
|
|
11
|
+
*
|
|
12
|
+
* A **genuine** instrument is fitted to the drawn nuance line of a green roll
|
|
13
|
+
* and says what a green Welte did. A **derived** instrument is fitted so that
|
|
14
|
+
* the green code of a recording reproduces what the fitted T-100 emulator makes
|
|
15
|
+
* of the *red* copy of the same recording, and says what Welte's editor meant
|
|
16
|
+
* the green roll to sound like; its provenance names the T-100 instrument it
|
|
17
|
+
* inherits, since it is only as good as the red reading behind it. The
|
|
18
|
+
* difference between the two, in the printed ordinate both scales share, is how
|
|
19
|
+
* far the transfer of a red reading onto the green mechanism succeeded.
|
|
20
|
+
*
|
|
21
|
+
* Neither has been fitted yet, so both groups are empty and what a playback runs
|
|
22
|
+
* on until then is the third group: the **unfitted** starting values, arithmetic
|
|
23
|
+
* from Welte's regulation controls and the T-100 consensus with no green roll
|
|
24
|
+
* behind any of it. A curve produced with them says so in its own `instrument`
|
|
25
|
+
* field, and nothing should be published from them.
|
|
26
|
+
*/
|
|
27
|
+
export declare const instruments: {
|
|
28
|
+
genuine: Readonly<Partial<Record<"consensus" | import("welte-mignon-emulator/t98").GenuineRoll, import("welte-mignon-emulator/t98").WelteT98Instrument>>>;
|
|
29
|
+
derived: Readonly<Partial<Record<"225", import("welte-mignon-emulator/t98").WelteT98Instrument>>>;
|
|
30
|
+
unfitted: {
|
|
31
|
+
'starting-values': import("welte-mignon-emulator/t98").WelteT98Instrument;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export declare const instrumentNames: readonly WelteT98InstrumentName[];
|
|
35
|
+
/** The instrument a pair of nuancing constants belongs to, if it is one. */
|
|
36
|
+
export declare const instrumentNameOf: (nuance: Record<Half, Parameters>) => WelteT98InstrumentName | undefined;
|
|
37
|
+
export { instrumentT98Of, labelOf, nuanceOf };
|
|
38
|
+
/**
|
|
39
|
+
* The two readings of the pedal mechanism, which is the T-100's below the
|
|
40
|
+
* command: Hagmann has the valves and bellows that carry out the movements "in
|
|
41
|
+
* beiden Systemen dieselbe" (p. 106), and only the Vorpneumatik differs.
|
|
42
|
+
*/
|
|
43
|
+
export declare const pedalPresets: {
|
|
44
|
+
damping: Readonly<Record<string, number>>;
|
|
45
|
+
brushing: Readonly<Record<string, number>>;
|
|
46
|
+
};
|
|
47
|
+
export type PedalPreset = keyof typeof pedalPresets;
|
|
48
|
+
export declare const pedalPresetOf: (pedals: Parameters) => PedalPreset | undefined;
|
|
49
|
+
export type WelteT98Options = {
|
|
50
|
+
/**
|
|
51
|
+
* The take-up spool, which sets the time axis. No source states a T-98
|
|
52
|
+
* spool geometry, so the default keeps the red circumference and layer and
|
|
53
|
+
* sets the revolution to make the initial paper speed 220 cm/min. It is
|
|
54
|
+
* better varied than trusted: it scales every conductance by k and every
|
|
55
|
+
* time constant by 1/k, and touches nothing dimensionless.
|
|
56
|
+
*/
|
|
57
|
+
spool: Spool;
|
|
58
|
+
/** Constants of the nuancing mechanism, one set for each half of the keyboard. */
|
|
59
|
+
nuance: Record<Half, Parameters>;
|
|
60
|
+
/** Which instrument those constants are, so that a curve can say so. */
|
|
61
|
+
instrument: WelteT98InstrumentName;
|
|
62
|
+
/** Constants of the two pedal actions, one of `pedalPresets` or a set of one's own. */
|
|
63
|
+
pedals: Parameters;
|
|
64
|
+
velocity: VelocityMap;
|
|
65
|
+
pedalMode: PedalMode;
|
|
66
|
+
/** Diameter of the tracker-bar bore. No source gives the T-98's; the red figure is carried over. */
|
|
67
|
+
trackerBore: Millimeters;
|
|
68
|
+
/** Punch diameter for an edition whose copies record none. */
|
|
69
|
+
punchDiameter: Millimeters;
|
|
70
|
+
/**
|
|
71
|
+
* Chained punches whose gap is shorter than this are one perforation. A held
|
|
72
|
+
* T-98 command is punched as a chain of round holes on a 2.66 mm grid with
|
|
73
|
+
* paper bridges of about a millimetre, not as one slot, so a raw scan and an
|
|
74
|
+
* edition whose collation has already made one symbol of the chain see the
|
|
75
|
+
* same port only if the chain is merged.
|
|
76
|
+
*/
|
|
77
|
+
chainGap: Millimeters;
|
|
78
|
+
/**
|
|
79
|
+
* The track at which the keyboard is divided, so that notes from here
|
|
80
|
+
* upwards follow the treble expression and the ones below it the bass.
|
|
81
|
+
* Welte puts the division between f♯ and g (Betriebsanleitung p. 7), which
|
|
82
|
+
* on this bar is track 52; PlaySK's green configuration independently gives
|
|
83
|
+
* the last bass note as MIDI 66. Which side an expression perforation
|
|
84
|
+
* belongs to is not decided here but read off the tracker bar.
|
|
85
|
+
*
|
|
86
|
+
* A green roll re-cut from a Mignon master uses only the middle 80 of the 88
|
|
87
|
+
* note positions, so the division falls inside the used compass either way.
|
|
88
|
+
*/
|
|
89
|
+
division: Track;
|
|
90
|
+
/**
|
|
91
|
+
* What a long perforation on the bass sforzando-piano line does. It is the
|
|
92
|
+
* same valve and the same hole as the dynamic (Skala-Rolle §10), so it
|
|
93
|
+
* always acts as a sforzando-piano; `stop` additionally ends the performance
|
|
94
|
+
* there, which is what the instrument does — during rewind the suction to
|
|
95
|
+
* the primary pneumatics is cut and the Hauptventil closes, so no note
|
|
96
|
+
* sounds and the expression apparatus is out of action (Betriebsanleitung
|
|
97
|
+
* p. 17). `ignore` is for an edition that is a fragment, or that carries a
|
|
98
|
+
* rewind in the middle for some reason of its own.
|
|
99
|
+
*/
|
|
100
|
+
rewind: 'stop' | 'ignore';
|
|
101
|
+
};
|
|
102
|
+
export declare const defaultWelteT98Options: WelteT98Options;
|
|
103
|
+
/** When the spool brings a place on the roll to the tracker bar. */
|
|
104
|
+
export declare const secondsAt: (spool: Spool, place: Millimeters) => Seconds;
|
|
105
|
+
/**
|
|
106
|
+
* The green Welte, as welte-mignon-emulator models it. Everything downstream of
|
|
107
|
+
* the relay is the T-100's, on Hagmann's authority that the nuancing unit is
|
|
108
|
+
* built the same for both tracker scales (p. 96); what differs is in front of
|
|
109
|
+
* it. Each function is held for exactly as long as its own perforation runs
|
|
110
|
+
* over the glide block, four conduits stand on one bellows and their drives add
|
|
111
|
+
* as flows, the crescendo's ceiling is the balance of throttle 98 against the
|
|
112
|
+
* permanently open bore 100, and a long perforation on the bass sforzando-piano
|
|
113
|
+
* line sends the roll back.
|
|
114
|
+
*
|
|
115
|
+
* Its constants are **not fitted**. `instruments.genuine` and
|
|
116
|
+
* `instruments.derived` are empty until their fits run, and what a playback runs
|
|
117
|
+
* on until then is the unfitted starting values, which every curve says.
|
|
118
|
+
*/
|
|
119
|
+
export declare const welteT98System: ReproducingSystem<WelteT98Options>;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { geometryInMm, Grid, levelChanges, mezzoforteTravel, paperSeconds, pedalBrushing, pedalDefaults, ROWS_PER_MM, travelBetweenRails, WELTE_T98_SPOOL, } from "welte-mignon-emulator";
|
|
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
|
+
import { welteT98 } from "./bar";
|
|
4
|
+
import { inCentimeters, mm, seconds, track } from "../../Quantity";
|
|
5
|
+
import { partitionPoint } from "../../sorted";
|
|
6
|
+
import { defaultVelocityMap, velocityOf } from "../velocity";
|
|
7
|
+
/**
|
|
8
|
+
* The instruments the emulator offers for the green Welte, in three groups that
|
|
9
|
+
* answer three different questions and must not be listed as one.
|
|
10
|
+
*
|
|
11
|
+
* A **genuine** instrument is fitted to the drawn nuance line of a green roll
|
|
12
|
+
* and says what a green Welte did. A **derived** instrument is fitted so that
|
|
13
|
+
* the green code of a recording reproduces what the fitted T-100 emulator makes
|
|
14
|
+
* of the *red* copy of the same recording, and says what Welte's editor meant
|
|
15
|
+
* the green roll to sound like; its provenance names the T-100 instrument it
|
|
16
|
+
* inherits, since it is only as good as the red reading behind it. The
|
|
17
|
+
* difference between the two, in the printed ordinate both scales share, is how
|
|
18
|
+
* far the transfer of a red reading onto the green mechanism succeeded.
|
|
19
|
+
*
|
|
20
|
+
* Neither has been fitted yet, so both groups are empty and what a playback runs
|
|
21
|
+
* on until then is the third group: the **unfitted** starting values, arithmetic
|
|
22
|
+
* from Welte's regulation controls and the T-100 consensus with no green roll
|
|
23
|
+
* behind any of it. A curve produced with them says so in its own `instrument`
|
|
24
|
+
* field, and nothing should be published from them.
|
|
25
|
+
*/
|
|
26
|
+
export const instruments = { genuine: GENUINE, derived: DERIVED, unfitted: { 'starting-values': STARTING_VALUES } };
|
|
27
|
+
export const instrumentNames = [
|
|
28
|
+
...Object.keys(GENUINE).map(genuine => ({ genuine })),
|
|
29
|
+
...Object.keys(DERIVED).map(derived => ({ derived })),
|
|
30
|
+
{ unfitted: 'starting-values' }
|
|
31
|
+
];
|
|
32
|
+
const sameParameters = (a, b) => Object.keys(a).length === Object.keys(b).length
|
|
33
|
+
&& Object.entries(a).every(([name, value]) => b[name] === value);
|
|
34
|
+
/** The instrument a pair of nuancing constants belongs to, if it is one. */
|
|
35
|
+
export const instrumentNameOf = (nuance) => instrumentNames.find(name => {
|
|
36
|
+
const instrument = instrumentT98Of(name);
|
|
37
|
+
return instrument !== undefined
|
|
38
|
+
&& sameParameters(instrument.bass, nuance.bass)
|
|
39
|
+
&& sameParameters(instrument.treble, nuance.treble);
|
|
40
|
+
});
|
|
41
|
+
export { instrumentT98Of, labelOf, nuanceOf };
|
|
42
|
+
/**
|
|
43
|
+
* The two readings of the pedal mechanism, which is the T-100's below the
|
|
44
|
+
* command: Hagmann has the valves and bellows that carry out the movements "in
|
|
45
|
+
* beiden Systemen dieselbe" (p. 106), and only the Vorpneumatik differs.
|
|
46
|
+
*/
|
|
47
|
+
export const pedalPresets = {
|
|
48
|
+
damping: pedalDefaults,
|
|
49
|
+
brushing: pedalBrushing
|
|
50
|
+
};
|
|
51
|
+
export const pedalPresetOf = (pedals) => Object.keys(pedalPresets).find(name => sameParameters(pedalPresets[name], pedals));
|
|
52
|
+
export const defaultWelteT98Options = {
|
|
53
|
+
spool: WELTE_T98_SPOOL,
|
|
54
|
+
nuance: nuanceOf(STARTING_VALUES),
|
|
55
|
+
instrument: { unfitted: 'starting-values' },
|
|
56
|
+
pedals: pedalPresets.damping,
|
|
57
|
+
velocity: defaultVelocityMap,
|
|
58
|
+
pedalMode: 'continuous',
|
|
59
|
+
trackerBore: mm(TRACKER_BORE_T98_MM),
|
|
60
|
+
punchDiameter: mm(PUNCH_T98_MM),
|
|
61
|
+
chainGap: mm(CHAIN_GAP_MM),
|
|
62
|
+
division: track(52),
|
|
63
|
+
rewind: 'stop'
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* What each expression code operates, in the emulator's terms. There is no
|
|
67
|
+
* action: a T-98 function lasts exactly as long as its perforation, so a symbol
|
|
68
|
+
* carries its own extent and nothing cancels it. `SoftPedal` maps to the
|
|
69
|
+
* emulator's `hammerRail`, which keeps Hagmann's part name rather than the
|
|
70
|
+
* roll's function name.
|
|
71
|
+
*/
|
|
72
|
+
const CODES = {
|
|
73
|
+
SforzandoPiano: 'sforzandoPiano',
|
|
74
|
+
SforzandoForte: 'sforzandoForte',
|
|
75
|
+
Mezzoforte: 'mezzoforte',
|
|
76
|
+
Crescendo: 'crescendo',
|
|
77
|
+
SustainPedal: 'sustainPedal',
|
|
78
|
+
SoftPedal: 'hammerRail'
|
|
79
|
+
};
|
|
80
|
+
const isWelteT98ExpressionType = (type) => Object.hasOwn(CODES, type);
|
|
81
|
+
const codeOf = (expressionType) => isWelteT98ExpressionType(expressionType) ? CODES[expressionType] : undefined;
|
|
82
|
+
/** Paper the grid runs on past the last hole, so that a final pedal release completes. */
|
|
83
|
+
const RUN_OUT = mm(100);
|
|
84
|
+
const isNote = (event) => event.type === 'note';
|
|
85
|
+
const isExpression = (event) => event.type === 'expression';
|
|
86
|
+
const paperOf = (toOwnPaper) => ({
|
|
87
|
+
rowOf: place => place * toOwnPaper * ROWS_PER_MM,
|
|
88
|
+
placeOfRow: row => mm(row / (toOwnPaper * ROWS_PER_MM)),
|
|
89
|
+
paperOfRow: row => mm(row / ROWS_PER_MM)
|
|
90
|
+
});
|
|
91
|
+
/** When the spool brings a place on the roll to the tracker bar. */
|
|
92
|
+
export const secondsAt = (spool, place) => seconds(paperSeconds(spool, inCentimeters(place)));
|
|
93
|
+
const halfOf = (note, division) => note.vertical.from >= division ? 'treble' : 'bass';
|
|
94
|
+
const readingOf = (paper) => (event) => {
|
|
95
|
+
const control = codeOf(event.expressionType);
|
|
96
|
+
if (!control)
|
|
97
|
+
return undefined;
|
|
98
|
+
const half = event.scope;
|
|
99
|
+
return {
|
|
100
|
+
event,
|
|
101
|
+
punch: {
|
|
102
|
+
half,
|
|
103
|
+
control,
|
|
104
|
+
rowOn: paper.rowOf(event.horizontal.from),
|
|
105
|
+
rowOff: paper.rowOf(event.horizontal.to)
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
110
|
+
/**
|
|
111
|
+
* One sample per row of the scan the constants were fitted on, from the
|
|
112
|
+
* beginning of the roll to a little past the last hole.
|
|
113
|
+
*/
|
|
114
|
+
const gridOver = (events, spool, paper) => {
|
|
115
|
+
const last = mm(events.reduce((furthest, event) => Math.max(furthest, event.horizontal.to), 0));
|
|
116
|
+
const length = Math.ceil(paper.rowOf(last) + RUN_OUT * ROWS_PER_MM) + 1;
|
|
117
|
+
const times = new Float64Array(length).map((_, row) => secondsAt(spool, paper.paperOfRow(row)));
|
|
118
|
+
return new Grid(0, times);
|
|
119
|
+
};
|
|
120
|
+
const nuanceCurves = (grid, ports, samples, options) => {
|
|
121
|
+
const instrument = `Welte-Mignon T-98, ${labelOf(options.instrument)}`;
|
|
122
|
+
const curveOf = (half) => {
|
|
123
|
+
const params = options.nuance[half];
|
|
124
|
+
const output = pneumaticT98Model.run({ grid, half, ports }, params);
|
|
125
|
+
const travel = travelBetweenRails(output, params);
|
|
126
|
+
const hook = clamp(mezzoforteTravel(params), 0.01, 0.99);
|
|
127
|
+
return {
|
|
128
|
+
...samples,
|
|
129
|
+
name: half,
|
|
130
|
+
kind: 'dynamics',
|
|
131
|
+
instrument,
|
|
132
|
+
travel,
|
|
133
|
+
velocity: travel.map(value => velocityOf(value, hook, options.velocity))
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
return { bass: curveOf('bass'), treble: curveOf('treble') };
|
|
137
|
+
};
|
|
138
|
+
const pedalCurves = (grid, ports, samples, options) => {
|
|
139
|
+
const travel = runPedals({ grid, ports }, options.pedals);
|
|
140
|
+
return {
|
|
141
|
+
damper: { ...samples, name: 'damper', kind: 'pedal', travel: travel.damper },
|
|
142
|
+
hammerRail: { ...samples, name: 'hammerRail', kind: 'pedal', travel: travel.hammerRail }
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
const performNotes = (events, grid, nuance, options, paper) => events
|
|
146
|
+
.filter(isNote)
|
|
147
|
+
.flatMap((note) => {
|
|
148
|
+
const curve = nuance[halfOf(note, options.division)];
|
|
149
|
+
const velocity = curve.velocity[grid.indexOfRow(paper.rowOf(note.horizontal.from))];
|
|
150
|
+
return [
|
|
151
|
+
{ type: 'noteOn', performs: note, pitch: note.pitch, velocity, at: secondsAt(options.spool, note.horizontal.from) },
|
|
152
|
+
{ type: 'noteOff', performs: note, pitch: note.pitch, velocity: 127, at: secondsAt(options.spool, note.horizontal.to) }
|
|
153
|
+
];
|
|
154
|
+
});
|
|
155
|
+
/**
|
|
156
|
+
* The travel of one pedal as controller steps, each attributed to the last
|
|
157
|
+
* perforation of that pedal the tracker bar has reached.
|
|
158
|
+
*/
|
|
159
|
+
const performPedal = (type, curve, grid, readings, mode) => {
|
|
160
|
+
if (readings.length === 0)
|
|
161
|
+
return [];
|
|
162
|
+
const ordered = readings.toSorted((a, b) => a.punch.rowOn - b.punch.rowOn);
|
|
163
|
+
const causeOf = (row) => ordered[Math.max(partitionPoint(ordered, reading => reading.punch.rowOn <= row) - 1, 0)].event;
|
|
164
|
+
return levelChanges(curve.travel, { mode })
|
|
165
|
+
.filter(change => change.index > 0)
|
|
166
|
+
.map(change => ({
|
|
167
|
+
type,
|
|
168
|
+
performs: causeOf(grid.rowAt(change.index)),
|
|
169
|
+
value: change.value,
|
|
170
|
+
at: seconds(curve.seconds[change.index])
|
|
171
|
+
}));
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* The row at which the Abstellbalg trips and the roll goes back, or the end of
|
|
175
|
+
* the grid where it never does. The shut-off hangs on the bass sforzando-piano
|
|
176
|
+
* valve and reads the same perforations the dynamic does, so this asks the
|
|
177
|
+
* emulator for that valve's lift and integrates it.
|
|
178
|
+
*/
|
|
179
|
+
const rewindRow = (grid, ports, options) => {
|
|
180
|
+
if (options.rewind === 'ignore')
|
|
181
|
+
return grid.length;
|
|
182
|
+
const lift = sforzandoPianoLift({ grid, half: 'bass', ports }, options.nuance.bass);
|
|
183
|
+
return rewindAt(lift, grid.dt) ?? grid.length;
|
|
184
|
+
};
|
|
185
|
+
const truncated = (events, until) => events.filter(event => event.at <= until);
|
|
186
|
+
const cutCurve = (curve, rows) => ({
|
|
187
|
+
...curve,
|
|
188
|
+
place: curve.place.slice(0, rows),
|
|
189
|
+
seconds: curve.seconds.slice(0, rows),
|
|
190
|
+
travel: curve.travel.slice(0, rows),
|
|
191
|
+
...(curve.kind === 'dynamics' ? { velocity: curve.velocity.slice(0, rows) } : {})
|
|
192
|
+
});
|
|
193
|
+
/**
|
|
194
|
+
* The edition's tempo adjustment is left aside, as on the T-100: it is stated as
|
|
195
|
+
* a paper speed, and what the spool holds constant is its rate of revolution.
|
|
196
|
+
*/
|
|
197
|
+
const perform = (events, options, roll) => {
|
|
198
|
+
const paper = paperOf(roll.toOwnPaper ?? 1);
|
|
199
|
+
const readings = events
|
|
200
|
+
.filter(isExpression)
|
|
201
|
+
.map(readingOf(paper))
|
|
202
|
+
.filter((reading) => reading !== undefined);
|
|
203
|
+
const grid = gridOver(events, options.spool, paper);
|
|
204
|
+
const geometry = geometryInMm(roll.punchDiameter ?? options.punchDiameter, options.trackerBore);
|
|
205
|
+
const gap = options.chainGap * ROWS_PER_MM;
|
|
206
|
+
const ports = aperturePorts(grid, readings.map(reading => reading.punch), geometry, gap);
|
|
207
|
+
const samples = {
|
|
208
|
+
place: grid.seconds.map((_, row) => paper.placeOfRow(row)),
|
|
209
|
+
seconds: grid.seconds
|
|
210
|
+
};
|
|
211
|
+
const nuance = nuanceCurves(grid, ports, samples, options);
|
|
212
|
+
const pedals = pedalCurves(grid, ports, samples, options);
|
|
213
|
+
const readingsOf = (control) => readings.filter(reading => reading.punch.control === control);
|
|
214
|
+
const stops = rewindRow(grid, ports, options);
|
|
215
|
+
const until = seconds(grid.seconds[Math.min(stops, grid.length - 1)]);
|
|
216
|
+
const rows = Math.min(stops + 1, grid.length);
|
|
217
|
+
return {
|
|
218
|
+
events: truncated([
|
|
219
|
+
...performNotes(events, grid, nuance, options, paper),
|
|
220
|
+
...performPedal('damper', pedals.damper, grid, readingsOf('sustainPedal'), options.pedalMode),
|
|
221
|
+
...performPedal('hammerRail', pedals.hammerRail, grid, readingsOf('hammerRail'), options.pedalMode)
|
|
222
|
+
], until),
|
|
223
|
+
curves: [nuance.bass, nuance.treble, pedals.damper, pedals.hammerRail].map(curve => cutCurve(curve, rows))
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* The green Welte, as welte-mignon-emulator models it. Everything downstream of
|
|
228
|
+
* the relay is the T-100's, on Hagmann's authority that the nuancing unit is
|
|
229
|
+
* built the same for both tracker scales (p. 96); what differs is in front of
|
|
230
|
+
* it. Each function is held for exactly as long as its own perforation runs
|
|
231
|
+
* over the glide block, four conduits stand on one bellows and their drives add
|
|
232
|
+
* as flows, the crescendo's ceiling is the balance of throttle 98 against the
|
|
233
|
+
* permanently open bore 100, and a long perforation on the bass sforzando-piano
|
|
234
|
+
* line sends the roll back.
|
|
235
|
+
*
|
|
236
|
+
* Its constants are **not fitted**. `instruments.genuine` and
|
|
237
|
+
* `instruments.derived` are empty until their fits run, and what a playback runs
|
|
238
|
+
* on until then is the unfitted starting values, which every curve says.
|
|
239
|
+
*/
|
|
240
|
+
export const welteT98System = {
|
|
241
|
+
name: 'Welte-Mignon T98',
|
|
242
|
+
trackerBar: welteT98,
|
|
243
|
+
defaultOptions: defaultWelteT98Options,
|
|
244
|
+
perform
|
|
245
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linked-rolls",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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": {
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
"types": "./lib/systems/welteT100/system.d.ts",
|
|
21
21
|
"default": "./lib/systems/welteT100/system.js"
|
|
22
22
|
},
|
|
23
|
+
"./welte-t98": {
|
|
24
|
+
"types": "./lib/systems/welteT98/system.d.ts",
|
|
25
|
+
"default": "./lib/systems/welteT98/system.js"
|
|
26
|
+
},
|
|
27
|
+
"./welte-licensee": {
|
|
28
|
+
"types": "./lib/systems/welteLicensee/system.d.ts",
|
|
29
|
+
"default": "./lib/systems/welteLicensee/system.js"
|
|
30
|
+
},
|
|
23
31
|
"./lib/*": "./lib/*",
|
|
24
32
|
"./package.json": "./package.json"
|
|
25
33
|
},
|
|
@@ -40,13 +48,13 @@
|
|
|
40
48
|
"typescript": "^7.0.2",
|
|
41
49
|
"vite": "^8.2.2",
|
|
42
50
|
"vitest": "^5.0.0",
|
|
43
|
-
"welte-
|
|
51
|
+
"welte-mignon-emulator": "^1.0.0"
|
|
44
52
|
},
|
|
45
53
|
"peerDependencies": {
|
|
46
|
-
"welte-
|
|
54
|
+
"welte-mignon-emulator": "^1.0.0"
|
|
47
55
|
},
|
|
48
56
|
"peerDependenciesMeta": {
|
|
49
|
-
"welte-
|
|
57
|
+
"welte-mignon-emulator": {
|
|
50
58
|
"optional": true
|
|
51
59
|
}
|
|
52
60
|
},
|