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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { defaultCollationTolerance } from "./Collation";
|
|
2
|
+
import { distance, mm, track } from "./Quantity";
|
|
3
|
+
/**
|
|
4
|
+
* What each Welte command operates.
|
|
5
|
+
*
|
|
6
|
+
* This cannot be read off the names. The T-100 latches a function on
|
|
7
|
+
* with one perforation and cancels it with a second, while the T-98
|
|
8
|
+
* holds it for as long as one perforation lasts (Hagmann pp. 89 f. and
|
|
9
|
+
* 100–103; Phillips p. 121), and Welte renamed two of the functions
|
|
10
|
+
* between the scales: the red `SlowCrescendo` is the green `Crescendo`,
|
|
11
|
+
* and the red's single `Forzando` valve answers to two green ones that
|
|
12
|
+
* name the end they pull towards. So the correspondence is stated here
|
|
13
|
+
* rather than derived, and only two commands that operate one function
|
|
14
|
+
* can stand for each other.
|
|
15
|
+
*
|
|
16
|
+
* The T-100's `MotorOn`/`MotorOff`, `Rewind` and `ElectricCutOff` are
|
|
17
|
+
* left out on purpose: the green scale has no word for any of them, its
|
|
18
|
+
* motor switch being an automatic mercury contact (Skala-Rolle 98 §12)
|
|
19
|
+
* and its rewind riding on the bass sforzando-piano line. A red command
|
|
20
|
+
* of those kinds has no counterpart and stays a plain deletion.
|
|
21
|
+
*/
|
|
22
|
+
const commands = {
|
|
23
|
+
MezzoforteOn: { operates: 'mezzoforte', spelling: 'on', sided: true },
|
|
24
|
+
MezzoforteOff: { operates: 'mezzoforte', spelling: 'off', sided: true },
|
|
25
|
+
SlowCrescendoOn: { operates: 'crescendo', spelling: 'on', sided: true },
|
|
26
|
+
SlowCrescendoOff: { operates: 'crescendo', spelling: 'off', sided: true },
|
|
27
|
+
ForzandoOn: { operates: 'sforzando', spelling: 'on', sided: true },
|
|
28
|
+
ForzandoOff: { operates: 'sforzando', spelling: 'off', sided: true },
|
|
29
|
+
SustainPedalOn: { operates: 'sustainPedal', spelling: 'on', sided: false },
|
|
30
|
+
SustainPedalOff: { operates: 'sustainPedal', spelling: 'off', sided: false },
|
|
31
|
+
SoftPedalOn: { operates: 'softPedal', spelling: 'on', sided: false },
|
|
32
|
+
SoftPedalOff: { operates: 'softPedal', spelling: 'off', sided: false },
|
|
33
|
+
Mezzoforte: { operates: 'mezzoforte', spelling: 'held', sided: true },
|
|
34
|
+
Crescendo: { operates: 'crescendo', spelling: 'held', sided: true },
|
|
35
|
+
SforzandoForte: { operates: 'sforzando', spelling: 'held', sided: true },
|
|
36
|
+
SforzandoPiano: { operates: 'sforzando', spelling: 'held', sided: true },
|
|
37
|
+
SustainPedal: { operates: 'sustainPedal', spelling: 'held', sided: false },
|
|
38
|
+
SoftPedal: { operates: 'softPedal', spelling: 'held', sided: false }
|
|
39
|
+
};
|
|
40
|
+
/** What a command operates, or nothing for a word no scale here knows. */
|
|
41
|
+
export const commandOf = (expressionType) => commands[expressionType];
|
|
42
|
+
/**
|
|
43
|
+
* How wide a paper bridge may be for two held perforations to be one
|
|
44
|
+
* command. The T-98 punches a hold as a chain of round holes on a
|
|
45
|
+
* 2.66 mm grid with bridges of about a millimetre, so a scan read hole
|
|
46
|
+
* by hole shows a run where the paper shows one command.
|
|
47
|
+
*/
|
|
48
|
+
export const defaultChainGap = mm(3);
|
|
49
|
+
const isExpression = (symbol) => symbol.type === 'expression';
|
|
50
|
+
/** The key a command is matched on: the function, and the side where the side counts. */
|
|
51
|
+
const functionKey = (symbol, command) => command.sided ? `${command.operates} ${symbol.scope}` : command.operates;
|
|
52
|
+
/**
|
|
53
|
+
* The position a bar reads its own command for the same function on,
|
|
54
|
+
* where it has no word for this one.
|
|
55
|
+
*
|
|
56
|
+
* A version keeps the perforations it does away with in its deletions,
|
|
57
|
+
* and those may be a scale the version is not coded for: a green
|
|
58
|
+
* version deletes the red `SlowCrescendoOn` it inherits. That has no
|
|
59
|
+
* position on the green bar, so drawing it as a perforation is out of
|
|
60
|
+
* the question, but an edit still has to be shown somewhere, and the
|
|
61
|
+
* lane the green scale gives the same function is where it belongs.
|
|
62
|
+
*/
|
|
63
|
+
export const positionOfSameFunction = (bar, symbol) => {
|
|
64
|
+
const wanted = commandOf(symbol.expressionType);
|
|
65
|
+
if (!wanted)
|
|
66
|
+
return undefined;
|
|
67
|
+
const positions = Array.from({ length: bar.trackCount }, (_, index) => track(index + 1));
|
|
68
|
+
return positions.find(position => {
|
|
69
|
+
const meaning = bar.meaningOf(position);
|
|
70
|
+
if (meaning?.type !== 'expression')
|
|
71
|
+
return false;
|
|
72
|
+
const command = commandOf(meaning.expressionType);
|
|
73
|
+
return command !== undefined
|
|
74
|
+
&& command.operates === wanted.operates
|
|
75
|
+
&& functionKey(meaning, command) === functionKey(symbol, wanted);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The intervals the older version latches: each perforation that turns
|
|
80
|
+
* a function on, with the one that cancels it next on the same line.
|
|
81
|
+
*/
|
|
82
|
+
const latchedIn = (symbols, locate) => {
|
|
83
|
+
const byFunction = new Map();
|
|
84
|
+
symbols.filter(isExpression).forEach(symbol => {
|
|
85
|
+
const command = commandOf(symbol.expressionType);
|
|
86
|
+
const at = locate(symbol)?.from;
|
|
87
|
+
if (!command || command.spelling === 'held' || at === undefined)
|
|
88
|
+
return;
|
|
89
|
+
const key = functionKey(symbol, command);
|
|
90
|
+
const group = byFunction.get(key) ?? [];
|
|
91
|
+
group.push({ symbol, at, spelling: command.spelling });
|
|
92
|
+
byFunction.set(key, group);
|
|
93
|
+
});
|
|
94
|
+
return [...byFunction.values()].flatMap(group => {
|
|
95
|
+
const inOrder = [...group].sort((a, b) => a.at - b.at);
|
|
96
|
+
return inOrder.reduce((state, entry) => {
|
|
97
|
+
// A second On while the function already stands is
|
|
98
|
+
// redundant, and the interval began at the first, which
|
|
99
|
+
// is how a red copy reads that reaches the end of the
|
|
100
|
+
// roll with two Ons and one Off.
|
|
101
|
+
if (entry.spelling === 'on') {
|
|
102
|
+
return state.open ? state : { ...state, open: { on: entry.symbol, from: entry.at } };
|
|
103
|
+
}
|
|
104
|
+
return state.open
|
|
105
|
+
? {
|
|
106
|
+
open: undefined,
|
|
107
|
+
closed: [...state.closed, {
|
|
108
|
+
on: state.open.on,
|
|
109
|
+
off: entry.symbol,
|
|
110
|
+
from: state.open.from,
|
|
111
|
+
to: entry.at,
|
|
112
|
+
key: functionKey(state.open.on, commandOf(state.open.on.expressionType))
|
|
113
|
+
}]
|
|
114
|
+
}
|
|
115
|
+
: state;
|
|
116
|
+
}, { closed: [] }).closed;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The commands the newer version holds, a chain of punches counting as
|
|
121
|
+
* the one command the paper shows rather than as a run of them.
|
|
122
|
+
*/
|
|
123
|
+
const heldRunsIn = (symbols, locate, chainGap) => {
|
|
124
|
+
const byFunction = new Map();
|
|
125
|
+
symbols.filter(isExpression).forEach(symbol => {
|
|
126
|
+
const command = commandOf(symbol.expressionType);
|
|
127
|
+
const place = locate(symbol);
|
|
128
|
+
if (!command || command.spelling !== 'held' || !place)
|
|
129
|
+
return;
|
|
130
|
+
const key = functionKey(symbol, command);
|
|
131
|
+
const group = byFunction.get(key) ?? [];
|
|
132
|
+
group.push({ symbol, from: place.from, to: place.to });
|
|
133
|
+
byFunction.set(key, group);
|
|
134
|
+
});
|
|
135
|
+
return [...byFunction.entries()].flatMap(([key, group]) => [...group]
|
|
136
|
+
.sort((a, b) => a.from - b.from)
|
|
137
|
+
.reduce((runs, punch) => {
|
|
138
|
+
const open = runs[runs.length - 1];
|
|
139
|
+
if (open && punch.from - open.to <= chainGap) {
|
|
140
|
+
open.of.push(punch.symbol);
|
|
141
|
+
open.to = punch.to > open.to ? punch.to : open.to;
|
|
142
|
+
return runs;
|
|
143
|
+
}
|
|
144
|
+
return [...runs, { of: [punch.symbol], from: punch.from, to: punch.to, key }];
|
|
145
|
+
}, []));
|
|
146
|
+
};
|
|
147
|
+
/** The only item of the list, or nothing where there is none or a rival. */
|
|
148
|
+
const theOnly = (items) => items.length === 1 ? items[0] : undefined;
|
|
149
|
+
/**
|
|
150
|
+
* Where a held perforation of the newer version stands for a latched
|
|
151
|
+
* function of the older one: the same function, on the same side where
|
|
152
|
+
* the side counts, spanning the same stretch of the roll.
|
|
153
|
+
*
|
|
154
|
+
* Only unambiguous correspondences are reported. Where two held
|
|
155
|
+
* perforations answer one latched interval, or one answers two, none of
|
|
156
|
+
* them is reported and the editor is left to say what happened. Nothing
|
|
157
|
+
* is invented either: both sides are perforations somebody punched, and
|
|
158
|
+
* all this asserts is which stands for which.
|
|
159
|
+
*/
|
|
160
|
+
export const substitutionsBetween = (own, inherited, locate, tolerance = defaultCollationTolerance, chainGap = defaultChainGap) => {
|
|
161
|
+
const latched = latchedIn(inherited, locate);
|
|
162
|
+
const spans = (run, interval) => distance(run.from, interval.from) <= tolerance.toleranceStart
|
|
163
|
+
&& distance(run.to, interval.to) <= tolerance.toleranceEnd;
|
|
164
|
+
const candidates = heldRunsIn(own, locate, chainGap).flatMap(run => {
|
|
165
|
+
const answered = latched.filter(interval => interval.key === run.key && spans(run, interval));
|
|
166
|
+
const only = theOnly(answered);
|
|
167
|
+
return only ? [{ run, interval: only }] : [];
|
|
168
|
+
});
|
|
169
|
+
// A latched interval two runs both answer is ambiguous from its side
|
|
170
|
+
// as well, so neither of them is reported.
|
|
171
|
+
const claims = candidates.reduce((counts, { interval }) => counts.set(interval.on.id, (counts.get(interval.on.id) ?? 0) + 1), new Map());
|
|
172
|
+
return candidates
|
|
173
|
+
.filter(({ interval }) => claims.get(interval.on.id) === 1)
|
|
174
|
+
.map(({ run, interval }) => ({ replaced: [interval.on, interval.off], by: run.of }));
|
|
175
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How bellows travel becomes a MIDI velocity.
|
|
3
|
+
*
|
|
4
|
+
* Shared by every Welte system rather than declared per system, and that is a
|
|
5
|
+
* decision about comparability rather than about accuracy. The point of running
|
|
6
|
+
* a red issue and a green issue of one recording through two emulators is to
|
|
7
|
+
* attribute the difference between them to the coding. If the two systems used
|
|
8
|
+
* two velocity maps, the difference between the maps would dominate and the
|
|
9
|
+
* comparison would measure the maps.
|
|
10
|
+
*
|
|
11
|
+
* Nothing in either mechanism determines the map. These three anchors are
|
|
12
|
+
* midi2exp's published velocities, joined linearly with the middle one at the
|
|
13
|
+
* Mezzoforte pin of the half in question, and they are what the T-100's
|
|
14
|
+
* published results were produced with. Two independent implementations
|
|
15
|
+
* disagree with them and may well be right: Stahnke gives `v_h = K_s √(w − W_s)`
|
|
16
|
+
* for vacuum to hammer velocity and `V_MIDI = round(52 + 25 log₂ v_h)` for
|
|
17
|
+
* velocity to MIDI, and Phillips and Gosden's *Rollmidi*, in the same volume,
|
|
18
|
+
* uses an explicitly logarithmic curve; Phillips's own bench measurement has the
|
|
19
|
+
* suction-to-velocity relation "essentially logarithmic" (pp. 215 f.). The
|
|
20
|
+
* default should change only once a red/green comparison has been run both ways
|
|
21
|
+
* and the difference measured, and then for both systems at once.
|
|
22
|
+
*/
|
|
23
|
+
export type VelocityMap = {
|
|
24
|
+
piano: number;
|
|
25
|
+
mezzoforte: number;
|
|
26
|
+
forte: number;
|
|
27
|
+
};
|
|
28
|
+
/** midi2exp's three velocities, which is what the T-100's published results used. */
|
|
29
|
+
export declare const defaultVelocityMap: VelocityMap;
|
|
30
|
+
/**
|
|
31
|
+
* The map joined linearly through its three anchors, with the middle one at the
|
|
32
|
+
* Mezzoforte pin of the half in question.
|
|
33
|
+
*/
|
|
34
|
+
export declare const velocityOf: (travel: number, hook: number, map: VelocityMap) => number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How bellows travel becomes a MIDI velocity.
|
|
3
|
+
*
|
|
4
|
+
* Shared by every Welte system rather than declared per system, and that is a
|
|
5
|
+
* decision about comparability rather than about accuracy. The point of running
|
|
6
|
+
* a red issue and a green issue of one recording through two emulators is to
|
|
7
|
+
* attribute the difference between them to the coding. If the two systems used
|
|
8
|
+
* two velocity maps, the difference between the maps would dominate and the
|
|
9
|
+
* comparison would measure the maps.
|
|
10
|
+
*
|
|
11
|
+
* Nothing in either mechanism determines the map. These three anchors are
|
|
12
|
+
* midi2exp's published velocities, joined linearly with the middle one at the
|
|
13
|
+
* Mezzoforte pin of the half in question, and they are what the T-100's
|
|
14
|
+
* published results were produced with. Two independent implementations
|
|
15
|
+
* disagree with them and may well be right: Stahnke gives `v_h = K_s √(w − W_s)`
|
|
16
|
+
* for vacuum to hammer velocity and `V_MIDI = round(52 + 25 log₂ v_h)` for
|
|
17
|
+
* velocity to MIDI, and Phillips and Gosden's *Rollmidi*, in the same volume,
|
|
18
|
+
* uses an explicitly logarithmic curve; Phillips's own bench measurement has the
|
|
19
|
+
* suction-to-velocity relation "essentially logarithmic" (pp. 215 f.). The
|
|
20
|
+
* default should change only once a red/green comparison has been run both ways
|
|
21
|
+
* and the difference measured, and then for both systems at once.
|
|
22
|
+
*/
|
|
23
|
+
/** midi2exp's three velocities, which is what the T-100's published results used. */
|
|
24
|
+
export const defaultVelocityMap = { piano: 35, mezzoforte: 60, forte: 90 };
|
|
25
|
+
const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
26
|
+
/**
|
|
27
|
+
* The map joined linearly through its three anchors, with the middle one at the
|
|
28
|
+
* Mezzoforte pin of the half in question.
|
|
29
|
+
*/
|
|
30
|
+
export const velocityOf = (travel, hook, map) => {
|
|
31
|
+
const position = clamp(travel, 0, 1);
|
|
32
|
+
if (position <= hook) {
|
|
33
|
+
return map.piano + (position / hook) * (map.mezzoforte - map.piano);
|
|
34
|
+
}
|
|
35
|
+
return map.mezzoforte + ((position - hook) / (1 - hook)) * (map.forte - map.mezzoforte);
|
|
36
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ReproducingSystem } from "../../ReproducingSystem";
|
|
2
|
+
import { type WelteT100Options } from "../welteT100/system";
|
|
3
|
+
export type WelteLicenseeOptions = WelteT100Options;
|
|
4
|
+
/**
|
|
5
|
+
* The one instrument on offer, and it is not a Licensee instrument.
|
|
6
|
+
*
|
|
7
|
+
* These are the T-100 consensus constants, fitted across the hand-drawn
|
|
8
|
+
* nuance lines of six rolls played on Freiburg instruments. Nothing here was
|
|
9
|
+
* measured on a Licensee: no Licensee roll is known to carry a drawn line, no
|
|
10
|
+
* Licensee instrument has been fitted, and the published prior art does not
|
|
11
|
+
* agree with itself — the same 186 ms is a part stroke in midi2exp and a whole
|
|
12
|
+
* stroke in pianolatron, a factor of 2.2.
|
|
13
|
+
*
|
|
14
|
+
* So a Licensee playback is a Freiburg reading of American paper, and the
|
|
15
|
+
* union has one arm to keep that visible. Nothing should be published from it.
|
|
16
|
+
*/
|
|
17
|
+
export declare const instruments: {
|
|
18
|
+
unfitted: {
|
|
19
|
+
'welte-t100-consensus': import("welte-mignon-emulator/t100").Instrument;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type WelteLicenseeInstrumentName = {
|
|
23
|
+
unfitted: 'welte-t100-consensus';
|
|
24
|
+
};
|
|
25
|
+
export declare const instrumentNames: readonly WelteLicenseeInstrumentName[];
|
|
26
|
+
/**
|
|
27
|
+
* Welte's own spool is carried over with the constants. Phillips (p. 181) says
|
|
28
|
+
* Licensee rolls "play at a range of paper speeds", so there is no one figure
|
|
29
|
+
* to put here and the red geometry is a placeholder rather than a reading.
|
|
30
|
+
* Where an edition states a tempo of its own it is the better authority, and
|
|
31
|
+
* the spool is better varied than trusted: it scales every conductance by k
|
|
32
|
+
* and every time constant by 1/k, and touches nothing dimensionless.
|
|
33
|
+
*/
|
|
34
|
+
export declare const defaultWelteLicenseeOptions: WelteLicenseeOptions;
|
|
35
|
+
/**
|
|
36
|
+
* Welte-Mignon (Licensee), the American re-cut. It keeps the T-100's
|
|
37
|
+
* lock-and-cancel coding on 98 tracks — which is what separates it from the
|
|
38
|
+
* green, the coding rule rather than the track count (Phillips, Table 4.3) —
|
|
39
|
+
* so it reads the same commands and is played by the same mechanism.
|
|
40
|
+
*
|
|
41
|
+
* What it does not have is an instrument of its own. The constants are the
|
|
42
|
+
* T-100's, the spool is the T-100's, and neither has ever been checked against
|
|
43
|
+
* a Licensee. `instruments` therefore offers a single `unfitted` arm, and every
|
|
44
|
+
* curve produced here names itself as such.
|
|
45
|
+
*/
|
|
46
|
+
export declare const welteLicenseeSystem: ReproducingSystem<WelteLicenseeOptions>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CONSENSUS } from "welte-mignon-emulator/t100";
|
|
2
|
+
import { track } from "../../Quantity";
|
|
3
|
+
import { welteLicensee } from "./bar";
|
|
4
|
+
import { defaultWelteT100Options, instrumentNameOf, nuanceOf, performAs } from "../welteT100/system";
|
|
5
|
+
/**
|
|
6
|
+
* The one instrument on offer, and it is not a Licensee instrument.
|
|
7
|
+
*
|
|
8
|
+
* These are the T-100 consensus constants, fitted across the hand-drawn
|
|
9
|
+
* nuance lines of six rolls played on Freiburg instruments. Nothing here was
|
|
10
|
+
* measured on a Licensee: no Licensee roll is known to carry a drawn line, no
|
|
11
|
+
* Licensee instrument has been fitted, and the published prior art does not
|
|
12
|
+
* agree with itself — the same 186 ms is a part stroke in midi2exp and a whole
|
|
13
|
+
* stroke in pianolatron, a factor of 2.2.
|
|
14
|
+
*
|
|
15
|
+
* So a Licensee playback is a Freiburg reading of American paper, and the
|
|
16
|
+
* union has one arm to keep that visible. Nothing should be published from it.
|
|
17
|
+
*/
|
|
18
|
+
export const instruments = { unfitted: { 'welte-t100-consensus': CONSENSUS } };
|
|
19
|
+
export const instrumentNames = [
|
|
20
|
+
{ unfitted: 'welte-t100-consensus' }
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Welte's own spool is carried over with the constants. Phillips (p. 181) says
|
|
24
|
+
* Licensee rolls "play at a range of paper speeds", so there is no one figure
|
|
25
|
+
* to put here and the red geometry is a placeholder rather than a reading.
|
|
26
|
+
* Where an edition states a tempo of its own it is the better authority, and
|
|
27
|
+
* the spool is better varied than trusted: it scales every conductance by k
|
|
28
|
+
* and every time constant by 1/k, and touches nothing dimensionless.
|
|
29
|
+
*/
|
|
30
|
+
export const defaultWelteLicenseeOptions = {
|
|
31
|
+
...defaultWelteT100Options,
|
|
32
|
+
nuance: nuanceOf(CONSENSUS),
|
|
33
|
+
/**
|
|
34
|
+
* Two positions lower than the T-100's 54: the Licensee drops the two
|
|
35
|
+
* motor tracks, so the note block and the treble valves sit two places in
|
|
36
|
+
* (Hagmann p. 40 f., Phillips p. 123).
|
|
37
|
+
*/
|
|
38
|
+
division: track(52)
|
|
39
|
+
};
|
|
40
|
+
/** Never the bare name of a T-100 instrument: a curve from here has to say what it ran on. */
|
|
41
|
+
const licenseeInstrument = (nuance) => instrumentNameOf(nuance) === 'consensus'
|
|
42
|
+
? 'Welte-Mignon (Licensee), unfitted — Welte-Mignon T-100 consensus'
|
|
43
|
+
: 'Welte-Mignon (Licensee), custom';
|
|
44
|
+
/**
|
|
45
|
+
* Welte-Mignon (Licensee), the American re-cut. It keeps the T-100's
|
|
46
|
+
* lock-and-cancel coding on 98 tracks — which is what separates it from the
|
|
47
|
+
* green, the coding rule rather than the track count (Phillips, Table 4.3) —
|
|
48
|
+
* so it reads the same commands and is played by the same mechanism.
|
|
49
|
+
*
|
|
50
|
+
* What it does not have is an instrument of its own. The constants are the
|
|
51
|
+
* T-100's, the spool is the T-100's, and neither has ever been checked against
|
|
52
|
+
* a Licensee. `instruments` therefore offers a single `unfitted` arm, and every
|
|
53
|
+
* curve produced here names itself as such.
|
|
54
|
+
*/
|
|
55
|
+
export const welteLicenseeSystem = {
|
|
56
|
+
name: 'Welte-Mignon (Licensee)',
|
|
57
|
+
trackerBar: welteLicensee,
|
|
58
|
+
defaultOptions: defaultWelteLicenseeOptions,
|
|
59
|
+
perform: performAs(licenseeInstrument)
|
|
60
|
+
};
|
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import { Half,
|
|
2
|
-
import {
|
|
1
|
+
import { Half, Parameters, PedalMode, Spool } from "welte-mignon-emulator";
|
|
2
|
+
import { Instrument, RollNumber } from "welte-mignon-emulator/t100";
|
|
3
|
+
import { NegotiatedEvent, Performance, ReproducingSystem, RollProperties } from "../../ReproducingSystem";
|
|
3
4
|
import { Millimeters, Seconds, Track } from "../../Quantity";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* mechanism determines how bellows travel maps onto hammer velocity, so
|
|
8
|
-
* these are anchors rather than measurements; the three values are the
|
|
9
|
-
* ones midi2exp publishes.
|
|
10
|
-
*/
|
|
11
|
-
export type VelocityMap = {
|
|
12
|
-
piano: number;
|
|
13
|
-
mezzoforte: number;
|
|
14
|
-
forte: number;
|
|
15
|
-
};
|
|
16
|
-
export type { Instrument } from "welte-t100-emulator";
|
|
5
|
+
import { type VelocityMap } from "../velocity";
|
|
6
|
+
export type { VelocityMap } from "../velocity";
|
|
7
|
+
export type { Instrument } from "welte-mignon-emulator/t100";
|
|
17
8
|
export type InstrumentName = 'consensus' | RollNumber;
|
|
18
9
|
/**
|
|
19
10
|
* The instruments the emulator was fitted as: the consensus over the six
|
|
@@ -76,7 +67,19 @@ export declare const defaultWelteT100Options: WelteT100Options;
|
|
|
76
67
|
/** When the spool brings a place on the roll to the tracker bar. */
|
|
77
68
|
export declare const secondsAt: (spool: Spool, place: Millimeters) => Seconds;
|
|
78
69
|
/**
|
|
79
|
-
* The
|
|
70
|
+
* The mechanism, given a way of naming the instrument on the curves it
|
|
71
|
+
* produces. The Licensee reads the same commands and is played by the same
|
|
72
|
+
* valves, so it shares this; what it may not share is the name, since a
|
|
73
|
+
* Licensee playback runs on constants fitted to Freiburg instruments and the
|
|
74
|
+
* 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
|
+
*/
|
|
80
|
+
export declare const performAs: (instrumentOf: (nuance: Record<Half, Parameters>) => string) => (events: readonly NegotiatedEvent[], options: WelteT100Options, roll: RollProperties) => Performance;
|
|
81
|
+
/**
|
|
82
|
+
* The red Welte, as welte-mignon-emulator models it: the take-up spool sets
|
|
80
83
|
* the time axis, the Nuancierbälge fill through their conduits and are
|
|
81
84
|
* arrested by the Mezzoforte pin, and the two pedals travel rather than
|
|
82
85
|
* switch. The constants are the consensus fitted across the hand-drawn
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
3
|
import { welteT100 } from "./bar";
|
|
3
|
-
import {
|
|
4
|
+
import { inCentimeters, mm, seconds, track } from "../../Quantity";
|
|
4
5
|
import { partitionPoint } from "../../sorted";
|
|
6
|
+
import { defaultVelocityMap, velocityOf } from "../velocity";
|
|
5
7
|
/**
|
|
6
8
|
* The instruments the emulator was fitted as: the consensus over the six
|
|
7
9
|
* rolls with drawn nuance lines, and the setting that drew each of them,
|
|
@@ -35,7 +37,7 @@ export const defaultWelteT100Options = {
|
|
|
35
37
|
spool: WELTE_SPOOL,
|
|
36
38
|
nuance: nuanceOf(instruments.consensus),
|
|
37
39
|
pedals: pedalPresets.damping,
|
|
38
|
-
velocity:
|
|
40
|
+
velocity: defaultVelocityMap,
|
|
39
41
|
pedalMode: 'continuous',
|
|
40
42
|
trackerBore: mm(TRACKER_BORE_MM),
|
|
41
43
|
punchDiameter: mm(DEFAULT_PUNCH_MM),
|
|
@@ -62,62 +64,47 @@ const isWelteT100ExpressionType = (type) => Object.hasOwn(CODES, type);
|
|
|
62
64
|
const codeOf = (expressionType) => isWelteT100ExpressionType(expressionType) ? CODES[expressionType] : undefined;
|
|
63
65
|
/** Paper the grid runs on past the last hole, so that a final pedal release completes. */
|
|
64
66
|
const RUN_OUT = mm(100);
|
|
65
|
-
/**
|
|
66
|
-
* Which stack of valves an expression perforation belongs to. The symbol
|
|
67
|
-
* usually says so, having been read off the tracker bar already; where it
|
|
68
|
-
* does not, the bar is asked again, since sending a perforation to neither
|
|
69
|
-
* side would quietly flatten the dynamics.
|
|
70
|
-
*/
|
|
71
|
-
const scopeOf = (event) => {
|
|
72
|
-
if (event.scope)
|
|
73
|
-
return event.scope;
|
|
74
|
-
const meaning = welteT100.meaningOf(event.vertical.from);
|
|
75
|
-
return meaning?.type === 'expression' ? meaning.scope : undefined;
|
|
76
|
-
};
|
|
77
67
|
const isNote = (event) => event.type === 'note';
|
|
78
68
|
const isExpression = (event) => event.type === 'expression';
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
});
|
|
82
74
|
/** When the spool brings a place on the roll to the tracker bar. */
|
|
83
75
|
export const secondsAt = (spool, place) => seconds(paperSeconds(spool, inCentimeters(place)));
|
|
84
76
|
const halfOf = (note, division) => note.vertical.from >= division ? 'treble' : 'bass';
|
|
85
|
-
const readingOf = (event) => {
|
|
77
|
+
const readingOf = (paper) => (event) => {
|
|
86
78
|
const code = codeOf(event.expressionType);
|
|
87
|
-
|
|
88
|
-
if (!code || !half)
|
|
79
|
+
if (!code)
|
|
89
80
|
return undefined;
|
|
81
|
+
const half = event.scope;
|
|
90
82
|
const [control, action] = code;
|
|
91
83
|
return {
|
|
92
84
|
event,
|
|
93
|
-
punch: {
|
|
85
|
+
punch: {
|
|
86
|
+
half,
|
|
87
|
+
control,
|
|
88
|
+
action,
|
|
89
|
+
rowOn: paper.rowOf(event.horizontal.from),
|
|
90
|
+
rowOff: paper.rowOf(event.horizontal.to)
|
|
91
|
+
}
|
|
94
92
|
};
|
|
95
93
|
};
|
|
96
94
|
const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
97
|
-
/**
|
|
98
|
-
* The velocity map joined linearly through its three anchors, with the
|
|
99
|
-
* middle one at the Mezzoforte pin of the half in question.
|
|
100
|
-
*/
|
|
101
|
-
const velocityOf = (travel, hook, map) => {
|
|
102
|
-
const position = clamp(travel, 0, 1);
|
|
103
|
-
if (position <= hook) {
|
|
104
|
-
return map.piano + (position / hook) * (map.mezzoforte - map.piano);
|
|
105
|
-
}
|
|
106
|
-
return map.mezzoforte + ((position - hook) / (1 - hook)) * (map.forte - map.mezzoforte);
|
|
107
|
-
};
|
|
108
95
|
/**
|
|
109
96
|
* One sample per row of the scan the constants were fitted on, from the
|
|
110
97
|
* beginning of the roll to a little past the last hole. The rows are
|
|
111
98
|
* equally spaced on the paper and not in time, which is what the
|
|
112
99
|
* emulator expects.
|
|
113
100
|
*/
|
|
114
|
-
const gridOver = (events, spool) => {
|
|
101
|
+
const gridOver = (events, spool, paper) => {
|
|
115
102
|
const last = mm(events.reduce((furthest, event) => Math.max(furthest, event.horizontal.to), 0));
|
|
116
|
-
const length = Math.ceil(rowOf(
|
|
117
|
-
const times = new Float64Array(length).map((_, row) => secondsAt(spool,
|
|
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)));
|
|
118
105
|
return new Grid(0, times);
|
|
119
106
|
};
|
|
120
|
-
const nuanceCurves = (grid, ports, samples, options) => {
|
|
107
|
+
const nuanceCurves = (grid, ports, samples, options, instrument) => {
|
|
121
108
|
const curveOf = (half) => {
|
|
122
109
|
const params = options.nuance[half];
|
|
123
110
|
const output = pneumaticModel.run({ grid, half, ports }, params);
|
|
@@ -127,6 +114,7 @@ const nuanceCurves = (grid, ports, samples, options) => {
|
|
|
127
114
|
...samples,
|
|
128
115
|
name: half,
|
|
129
116
|
kind: 'dynamics',
|
|
117
|
+
instrument,
|
|
130
118
|
travel,
|
|
131
119
|
velocity: travel.map(value => velocityOf(value, hook, options.velocity))
|
|
132
120
|
};
|
|
@@ -140,11 +128,11 @@ const pedalCurves = (grid, ports, samples, options) => {
|
|
|
140
128
|
hammerRail: { ...samples, name: 'hammerRail', kind: 'pedal', travel: travel.hammerRail }
|
|
141
129
|
};
|
|
142
130
|
};
|
|
143
|
-
const performNotes = (events, grid, nuance, options) => events
|
|
131
|
+
const performNotes = (events, grid, nuance, options, paper) => events
|
|
144
132
|
.filter(isNote)
|
|
145
133
|
.flatMap((note) => {
|
|
146
134
|
const curve = nuance[halfOf(note, options.division)];
|
|
147
|
-
const velocity = curve.velocity[grid.indexOfRow(rowOf(note.horizontal.from))];
|
|
135
|
+
const velocity = curve.velocity[grid.indexOfRow(paper.rowOf(note.horizontal.from))];
|
|
148
136
|
return [
|
|
149
137
|
{ type: 'noteOn', performs: note, pitch: note.pitch, velocity, at: secondsAt(options.spool, note.horizontal.from) },
|
|
150
138
|
{ type: 'noteOff', performs: note, pitch: note.pitch, velocity: 127, at: secondsAt(options.spool, note.horizontal.to) }
|
|
@@ -170,28 +158,35 @@ const performPedal = (type, curve, grid, readings, mode) => {
|
|
|
170
158
|
}));
|
|
171
159
|
};
|
|
172
160
|
/**
|
|
161
|
+
* The mechanism, given a way of naming the instrument on the curves it
|
|
162
|
+
* produces. The Licensee reads the same commands and is played by the same
|
|
163
|
+
* valves, so it shares this; what it may not share is the name, since a
|
|
164
|
+
* Licensee playback runs on constants fitted to Freiburg instruments and the
|
|
165
|
+
* curve has to say so.
|
|
166
|
+
*
|
|
173
167
|
* The edition's tempo adjustment is left aside: it is stated as a paper
|
|
174
168
|
* speed, and what the spool holds constant is its rate of revolution, so
|
|
175
169
|
* the two are not the same quantity. The spool in the options sets the speed.
|
|
176
170
|
*/
|
|
177
|
-
const
|
|
171
|
+
export const performAs = (instrumentOf) => (events, options, roll) => {
|
|
172
|
+
const paper = paperOf(roll.toOwnPaper ?? 1);
|
|
178
173
|
const readings = events
|
|
179
174
|
.filter(isExpression)
|
|
180
|
-
.map(readingOf)
|
|
175
|
+
.map(readingOf(paper))
|
|
181
176
|
.filter((reading) => reading !== undefined);
|
|
182
|
-
const grid = gridOver(events, options.spool);
|
|
177
|
+
const grid = gridOver(events, options.spool, paper);
|
|
183
178
|
const geometry = geometryInMm(roll.punchDiameter ?? options.punchDiameter, options.trackerBore);
|
|
184
179
|
const ports = aperturePorts(grid, readings.map(reading => reading.punch), geometry);
|
|
185
180
|
const samples = {
|
|
186
|
-
place: grid.seconds.map((_, row) => placeOfRow(row)),
|
|
181
|
+
place: grid.seconds.map((_, row) => paper.placeOfRow(row)),
|
|
187
182
|
seconds: grid.seconds
|
|
188
183
|
};
|
|
189
|
-
const nuance = nuanceCurves(grid, ports, samples, options);
|
|
184
|
+
const nuance = nuanceCurves(grid, ports, samples, options, instrumentOf(options.nuance));
|
|
190
185
|
const pedals = pedalCurves(grid, ports, samples, options);
|
|
191
186
|
const readingsOf = (control) => readings.filter(reading => reading.punch.control === control);
|
|
192
187
|
return {
|
|
193
188
|
events: [
|
|
194
|
-
...performNotes(events, grid, nuance, options),
|
|
189
|
+
...performNotes(events, grid, nuance, options, paper),
|
|
195
190
|
...performPedal('damper', pedals.damper, grid, readingsOf('sustainPedal'), options.pedalMode),
|
|
196
191
|
...performPedal('hammerRail', pedals.hammerRail, grid, readingsOf('hammerRail'), options.pedalMode)
|
|
197
192
|
],
|
|
@@ -199,7 +194,7 @@ const perform = (events, options, roll) => {
|
|
|
199
194
|
};
|
|
200
195
|
};
|
|
201
196
|
/**
|
|
202
|
-
* The red Welte, as welte-
|
|
197
|
+
* The red Welte, as welte-mignon-emulator models it: the take-up spool sets
|
|
203
198
|
* the time axis, the Nuancierbälge fill through their conduits and are
|
|
204
199
|
* arrested by the Mezzoforte pin, and the two pedals travel rather than
|
|
205
200
|
* switch. The constants are the consensus fitted across the hand-drawn
|
|
@@ -210,5 +205,5 @@ export const welteT100System = {
|
|
|
210
205
|
name: 'Welte-Mignon T100',
|
|
211
206
|
trackerBar: welteT100,
|
|
212
207
|
defaultOptions: defaultWelteT100Options,
|
|
213
|
-
perform
|
|
208
|
+
perform: performAs(nuance => `Welte-Mignon T-100, ${instrumentNameOf(nuance) ?? 'custom'}`)
|
|
214
209
|
};
|
|
@@ -25,7 +25,20 @@ export type WelteT98ExpressionType = typeof welteT98ExpressionTypes[number];
|
|
|
25
25
|
*
|
|
26
26
|
* Measured against Julian Dyer's scan of the T-98 copy of roll 225
|
|
27
27
|
* (Grünfeld, Träumerei): 98 columns 2.818 mm apart on paper 285.6 mm
|
|
28
|
-
* wide, notes on the middle 80 columns, the sustain valve punched
|
|
29
|
-
* times where the T-100 copy latches its sustain on
|
|
28
|
+
* wide, notes on the middle 80 columns, the sustain valve punched 52
|
|
29
|
+
* times in the music where the T-100 copy latches its sustain on 52
|
|
30
|
+
* times. A 53rd perforation on that track belongs to the test pattern
|
|
31
|
+
* after the rewind.
|
|
32
|
+
*
|
|
33
|
+
* The paper speed is the Deutsches Museum's figure for its Welte grün /
|
|
34
|
+
* T 98 rolls, 220 cm/min. Phillips gives 7 ft/min = 2.134 m/min (p. 121);
|
|
35
|
+
* midi2exp's and PlaySK's green defaults of 72.2 and 72.27, read as the
|
|
36
|
+
* Stanford convention of feet per minute times ten, are 2.201 m/min, which
|
|
37
|
+
* is the museum's figure; and the two copies of roll 225 last the same
|
|
38
|
+
* time if the green starts at about 7.30 ft/min. Welte's own booklets give
|
|
39
|
+
* no speed, only that the roll must run from the first "A" of the chromatic
|
|
40
|
+
* scale to the cross-line bearing the dial number in half a minute
|
|
41
|
+
* (Skala-Rolle 98 §1b). This is documentation; what sets the emulator's
|
|
42
|
+
* time axis is the spool.
|
|
30
43
|
*/
|
|
31
44
|
export declare const welteT98: TrackerBar;
|