linked-rolls 0.20.0 → 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/Emulation.js +4 -3
- package/lib/ReproducingSystem.d.ts +38 -2
- package/lib/RollCopy.d.ts +8 -0
- package/lib/RollCopy.js +12 -2
- package/lib/TrackerBar.d.ts +40 -0
- package/lib/TrackerBar.js +13 -1
- package/lib/context.d.ts +2 -0
- package/lib/context.js +2 -0
- package/lib/readers/phillipsEroll.d.ts +1 -1
- package/lib/spec/welte-t98.context.json +10 -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
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ tracker bar and a `perform` function; the core does not depend on any one
|
|
|
79
79
|
instrument's model.
|
|
80
80
|
|
|
81
81
|
The first system is the red Welte, `linked-rolls/welte-t100`, built on
|
|
82
|
-
[welte-
|
|
82
|
+
[welte-mignon-emulator](https://github.com/pfefferniels/welte-t100): the
|
|
83
83
|
take-up spool sets the time axis, the Nuancierbälge fill through their
|
|
84
84
|
conduits and are arrested by the Mezzoforte pin, and the two pedals travel
|
|
85
85
|
rather than switch. The nuancing constants come as `instruments`: the
|
|
@@ -103,11 +103,36 @@ emulation.emulateVersion(version, view)
|
|
|
103
103
|
const midi = emulation.asMIDI()
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
The
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
The second is the green Welte, `linked-rolls/welte-t98`. Everything
|
|
107
|
+
downstream of the relay is the same mechanism — Hagmann has the nuancing
|
|
108
|
+
unit built the same for both tracker scales (p. 96) — and what differs is
|
|
109
|
+
in front of it: each function lasts exactly as long as its own perforation
|
|
110
|
+
runs over the glide block rather than latching until a cancel line is read,
|
|
111
|
+
four conduits stand on one bellows and their drives add as flows, the
|
|
112
|
+
crescendo's ceiling is the balance of its throttle against a permanently
|
|
113
|
+
open bleed rather than a cap, the two pedals sit on the opposite edges of
|
|
114
|
+
the paper, and a long perforation on the bass sforzando-piano line sends
|
|
115
|
+
the roll back. Its constants are **not fitted**: `instruments.genuine` and
|
|
116
|
+
`instruments.derived` are empty until their fits are run, and until then a
|
|
117
|
+
playback runs on unfitted starting values, which every curve says in its
|
|
118
|
+
own `instrument` field.
|
|
119
|
+
|
|
120
|
+
Both systems share one velocity map, and `DynamicsCurve.travel` means the
|
|
121
|
+
same thing on both: the position on the printed ordinate of Welte's own
|
|
122
|
+
ruled band, 0 at that half's P.P. gridline and 1 at the shared F.F. line.
|
|
123
|
+
That is what lets a red issue and a green issue of one recording be
|
|
124
|
+
compared at all, since the band is ruled the same way on both.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { welteT98System } from 'linked-rolls/welte-t98'
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The emulator is an optional peer dependency: an application that uses one
|
|
131
|
+
of the Welte systems installs `welte-mignon-emulator` itself, and one that
|
|
132
|
+
only reads editions does not need it. For development on both at once,
|
|
133
|
+
`npm link` a checkout of
|
|
134
|
+
[welte-t100](https://github.com/pfefferniels/welte-t100) into this
|
|
135
|
+
repository.
|
|
111
136
|
|
|
112
137
|
### Alignment, order and pairing
|
|
113
138
|
|
package/lib/Emulation.js
CHANGED
|
@@ -9,9 +9,10 @@ const punchDiameterOf = (view) => {
|
|
|
9
9
|
.filter((value) => value !== undefined && value > 0);
|
|
10
10
|
return measured.length > 0 ? mean(measured) : undefined;
|
|
11
11
|
};
|
|
12
|
-
const propertiesOf = (view) => ({
|
|
12
|
+
const propertiesOf = (view, version) => ({
|
|
13
13
|
punchDiameter: punchDiameterOf(view),
|
|
14
|
-
tempo: view.edition.tempoAdjustment
|
|
14
|
+
tempo: view.edition.tempoAdjustment,
|
|
15
|
+
toOwnPaper: view.toOwnPaperOf(version)
|
|
15
16
|
});
|
|
16
17
|
/** The onset a symbol has on each copy carrying it, by the copy's index, as the mean of its holes there. */
|
|
17
18
|
const onsetsByCopy = (view, symbolId) => {
|
|
@@ -163,7 +164,7 @@ export class Emulation {
|
|
|
163
164
|
return this.midiEvents;
|
|
164
165
|
}
|
|
165
166
|
this.applyConstraints(view);
|
|
166
|
-
const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view));
|
|
167
|
+
const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view, version));
|
|
167
168
|
this.curves = performance.curves;
|
|
168
169
|
const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
|
|
169
170
|
const origin = skipToFirstNote ? earliestOf(onsets) : seconds(0);
|
|
@@ -63,8 +63,18 @@ interface CurveSamples {
|
|
|
63
63
|
readonly place: Float64Array;
|
|
64
64
|
readonly seconds: Float64Array;
|
|
65
65
|
/**
|
|
66
|
-
* Position
|
|
67
|
-
*
|
|
66
|
+
* Position on the printed ordinate of the roll's own ruled band: 0 at that
|
|
67
|
+
* half's P.P. gridline, 1 at the shared F.F. line, with M.F. at 0.5. For a
|
|
68
|
+
* pedal, 0 with it up and 1 with it down.
|
|
69
|
+
*
|
|
70
|
+
* Both Welte scales rule that band the same way — five rails, "P.P. M.F.
|
|
71
|
+
* F.F. M.F. P.P.", three named levels a side with the centre F.F. shared,
|
|
72
|
+
* measuring 20.0 mm between rails on red 3309 and on green Welte 184 alike —
|
|
73
|
+
* so a curve from one system and a curve from the other are in one unit, on
|
|
74
|
+
* Welte's authority rather than on ours. That is what lets a red issue and a
|
|
75
|
+
* green issue of one recording be compared at all. It is a unit of *drawn
|
|
76
|
+
* deflection*: whether the deflection is linear in the bellows' own travel is
|
|
77
|
+
* open, and is what the emulator's `scaleWarp` exists to answer.
|
|
68
78
|
*/
|
|
69
79
|
readonly travel: Float64Array;
|
|
70
80
|
}
|
|
@@ -72,6 +82,14 @@ interface CurveSamples {
|
|
|
72
82
|
export type DynamicsCurve = CurveSamples & {
|
|
73
83
|
readonly kind: 'dynamics';
|
|
74
84
|
readonly velocity: Float64Array;
|
|
85
|
+
/**
|
|
86
|
+
* The instrument whose constants produced it, by name. A comparison plot
|
|
87
|
+
* must not be able to put two curves side by side without saying which
|
|
88
|
+
* instrument each came from: a T-98 fitted to a green roll's drawn line and
|
|
89
|
+
* a T-98 fitted to a red copy's curve answer different questions, and their
|
|
90
|
+
* difference is the point of the comparison.
|
|
91
|
+
*/
|
|
92
|
+
readonly instrument: string;
|
|
75
93
|
};
|
|
76
94
|
export type PedalCurve = CurveSamples & {
|
|
77
95
|
readonly kind: 'pedal';
|
|
@@ -83,6 +101,24 @@ export type RollProperties = {
|
|
|
83
101
|
punchDiameter?: Millimeters;
|
|
84
102
|
/** The tempo the edition adjusts the roll to, where it states one. */
|
|
85
103
|
tempo?: RollTempo;
|
|
104
|
+
/**
|
|
105
|
+
* Place on the edition's shared axis × this = millimetres of the version's
|
|
106
|
+
* own paper. About 0.775 for a green issue of a red recording, 1 for a
|
|
107
|
+
* version on the axis it was measured on, which is the default.
|
|
108
|
+
*
|
|
109
|
+
* Versions of several systems share one place axis, so a green version's
|
|
110
|
+
* places are in red millimetres and would play about 29 % long if the
|
|
111
|
+
* spool read them as green paper. The factor is the inverse of the scale
|
|
112
|
+
* `alignCopy` recorded when it put the copy on the shared axis, taken from
|
|
113
|
+
* the copies of the version's own system: red copies carry a green
|
|
114
|
+
* version's notes too and say nothing about green paper.
|
|
115
|
+
*
|
|
116
|
+
* It scales **places only**. Punch diameter, the chain gap and the tracker
|
|
117
|
+
* bore are measured on the version's own paper already, and scaling them
|
|
118
|
+
* as well would put the aperture model out by the factor with nothing
|
|
119
|
+
* failing loudly.
|
|
120
|
+
*/
|
|
121
|
+
toOwnPaper?: number;
|
|
86
122
|
};
|
|
87
123
|
export type Performance = {
|
|
88
124
|
readonly events: readonly AnyPerformedRollFeature[];
|
package/lib/RollCopy.d.ts
CHANGED
|
@@ -320,6 +320,14 @@ export declare const barOf: (copy: Pick<RollCopy, 'production'>) => TrackerBar;
|
|
|
320
320
|
*
|
|
321
321
|
* The bar is named rather than defaulted: a copy is read by its own
|
|
322
322
|
* bar, and reading a green copy with the red one is a silent semitone.
|
|
323
|
+
*
|
|
324
|
+
* Reading stops where the bar says the roll's content ends. What a
|
|
325
|
+
* scanner punched past the rewind is not the roll speaking: Julian
|
|
326
|
+
* Dyer's scan of the green 225 carries a staircase of test punches
|
|
327
|
+
* across nearly every track after its rewind, which read on as symbols
|
|
328
|
+
* would be 318 notes nobody played. The features stay on the copy,
|
|
329
|
+
* since they are really on the paper and are his calibration of his own
|
|
330
|
+
* scan; it is the reading that stops.
|
|
323
331
|
*/
|
|
324
332
|
export declare function asSymbols(features: AnyFeature[], bar: TrackerBar): AnySymbol[];
|
|
325
333
|
/**
|
package/lib/RollCopy.js
CHANGED
|
@@ -26,10 +26,20 @@ export const barOf = (copy) => trackerBarOf(copy.production?.system) ?? welteT10
|
|
|
26
26
|
*
|
|
27
27
|
* The bar is named rather than defaulted: a copy is read by its own
|
|
28
28
|
* bar, and reading a green copy with the red one is a silent semitone.
|
|
29
|
+
*
|
|
30
|
+
* Reading stops where the bar says the roll's content ends. What a
|
|
31
|
+
* scanner punched past the rewind is not the roll speaking: Julian
|
|
32
|
+
* Dyer's scan of the green 225 carries a staircase of test punches
|
|
33
|
+
* across nearly every track after its rewind, which read on as symbols
|
|
34
|
+
* would be 318 notes nobody played. The features stay on the copy,
|
|
35
|
+
* since they are really on the paper and are his calibration of his own
|
|
36
|
+
* scan; it is the reading that stops.
|
|
29
37
|
*/
|
|
30
38
|
export function asSymbols(features, bar) {
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
const holes = features.filter(feature => feature.type === 'Hole');
|
|
40
|
+
const end = bar.endsAt(holes);
|
|
41
|
+
return holes
|
|
42
|
+
.filter(feature => end === undefined || feature.horizontal.from <= end.at)
|
|
33
43
|
.flatMap((feature) => {
|
|
34
44
|
const meaning = bar.meaningOf(feature.vertical.from);
|
|
35
45
|
if (!meaning)
|
package/lib/TrackerBar.d.ts
CHANGED
|
@@ -56,6 +56,17 @@ export interface TrackerBar {
|
|
|
56
56
|
* of their own, as the Licensee's do, states none.
|
|
57
57
|
*/
|
|
58
58
|
readonly paperSpeed?: SpeedMeasure;
|
|
59
|
+
/**
|
|
60
|
+
* Where the roll's own content ends, from its perforations alone, or
|
|
61
|
+
* `undefined` where nothing on the paper says. A copy that reads
|
|
62
|
+
* `undefined` runs to the end of whatever was scanned, which is a fact
|
|
63
|
+
* about the scan rather than about the roll and should be reported as one.
|
|
64
|
+
*
|
|
65
|
+
* Takes anything with a place and a position, so a reader can ask it of a
|
|
66
|
+
* copy's features before there are symbols, and a collation can ask it of
|
|
67
|
+
* the symbols afterwards.
|
|
68
|
+
*/
|
|
69
|
+
endsAt(features: readonly PlacedOnBar[]): RollEnd | undefined;
|
|
59
70
|
/** `undefined` for a position the bar does not read. */
|
|
60
71
|
meaningOf(position: Track): TrackMeaning | undefined;
|
|
61
72
|
/**
|
|
@@ -85,6 +96,28 @@ export declare const systemIdOf: (system: Concept | undefined) => string | undef
|
|
|
85
96
|
* in the bar's own 1-based numbering; `describeTrackerBar` gives them
|
|
86
97
|
* their type.
|
|
87
98
|
*/
|
|
99
|
+
/**
|
|
100
|
+
* Where a roll's own content ends, as far as its perforations say.
|
|
101
|
+
*
|
|
102
|
+
* This is a question about the paper, not about the mechanism: it asks where
|
|
103
|
+
* the rewind is *punched*, not when the rewind pneumatic takes hold. The second
|
|
104
|
+
* is a matter of valve lift and belongs to whatever performs the roll. Keeping
|
|
105
|
+
* them apart is what lets collation and counting ask this without an emulator.
|
|
106
|
+
*/
|
|
107
|
+
export type RollEnd = {
|
|
108
|
+
readonly at: Millimeters;
|
|
109
|
+
readonly because: 'rewind';
|
|
110
|
+
};
|
|
111
|
+
/** Anything carrying a place on the roll and a position on the bar. */
|
|
112
|
+
export type PlacedOnBar = {
|
|
113
|
+
readonly horizontal: {
|
|
114
|
+
readonly from: Millimeters;
|
|
115
|
+
readonly to: Millimeters;
|
|
116
|
+
};
|
|
117
|
+
readonly vertical: {
|
|
118
|
+
readonly from: Track;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
88
121
|
export interface TrackerBarSpec {
|
|
89
122
|
id: string;
|
|
90
123
|
name: string;
|
|
@@ -107,6 +140,13 @@ export interface TrackerBarSpec {
|
|
|
107
140
|
rewindTrack?: number;
|
|
108
141
|
/** The speed the system runs its rolls at, where the literature states one. */
|
|
109
142
|
paperSpeed?: SpeedMeasure;
|
|
143
|
+
/**
|
|
144
|
+
* How long a perforation on a *shared* rewind position has to be before it
|
|
145
|
+
* is the rewind rather than the command the position usually carries. Only
|
|
146
|
+
* meaningful with `rewindTrack`: a system that gives the rewind a line of
|
|
147
|
+
* its own needs no threshold, since anything there is the rewind.
|
|
148
|
+
*/
|
|
149
|
+
rewindHold?: Millimeters;
|
|
110
150
|
}
|
|
111
151
|
export declare const describeTrackerBar: (spec: TrackerBarSpec) => TrackerBar;
|
|
112
152
|
/**
|
package/lib/TrackerBar.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { track } from "./Quantity";
|
|
1
|
+
import { mm, track } from "./Quantity";
|
|
2
2
|
/**
|
|
3
3
|
* What a position says, as a key. Two bars read the same thing exactly
|
|
4
4
|
* where their keys agree, which is what lets a symbol cross from one
|
|
@@ -48,11 +48,23 @@ export const describeTrackerBar = (spec) => {
|
|
|
48
48
|
if (rewind === undefined) {
|
|
49
49
|
throw new Error(`${spec.name} declares no rewind track`);
|
|
50
50
|
}
|
|
51
|
+
// A rewind on a line of its own is unambiguous; one sharing a line is only
|
|
52
|
+
// the rewind when it is far longer than that line's usual command.
|
|
53
|
+
const shared = spec.rewindTrack !== undefined;
|
|
54
|
+
const endsAt = (features) => {
|
|
55
|
+
const hold = spec.rewindHold ?? mm(0);
|
|
56
|
+
const candidates = features
|
|
57
|
+
.filter(feature => feature.vertical.from === rewind)
|
|
58
|
+
.filter(feature => !shared || feature.horizontal.to - feature.horizontal.from >= hold)
|
|
59
|
+
.map(feature => feature.horizontal.from);
|
|
60
|
+
return candidates.length ? { at: mm(Math.min(...candidates)), because: 'rewind' } : undefined;
|
|
61
|
+
};
|
|
51
62
|
return {
|
|
52
63
|
id: spec.id,
|
|
53
64
|
name: spec.name,
|
|
54
65
|
width: spec.width,
|
|
55
66
|
trackCount: spec.trackCount,
|
|
67
|
+
endsAt,
|
|
56
68
|
areas,
|
|
57
69
|
expressionTypes: [...new Set(spec.expressions.values())],
|
|
58
70
|
rewindTrack: track(rewind),
|
package/lib/context.d.ts
CHANGED
|
@@ -8,3 +8,5 @@ export { default as welteT100JsonLdContext } from './spec/welte-t100.context.jso
|
|
|
8
8
|
* rather than minting a second set of IRIs for the same commands.
|
|
9
9
|
*/
|
|
10
10
|
export { default as welteLicenseeJsonLdContext } from './spec/welte-licensee.context.json';
|
|
11
|
+
/** The context of the Welte-Mignon T-98, added to an edition of a green roll. */
|
|
12
|
+
export { default as welteT98JsonLdContext } from './spec/welte-t98.context.json';
|
package/lib/context.js
CHANGED
|
@@ -8,3 +8,5 @@ export { default as welteT100JsonLdContext } from './spec/welte-t100.context.jso
|
|
|
8
8
|
* rather than minting a second set of IRIs for the same commands.
|
|
9
9
|
*/
|
|
10
10
|
export { default as welteLicenseeJsonLdContext } from './spec/welte-licensee.context.json';
|
|
11
|
+
/** The context of the Welte-Mignon T-98, added to an edition of a green roll. */
|
|
12
|
+
export { default as welteT98JsonLdContext } from './spec/welte-t98.context.json';
|
|
@@ -45,7 +45,7 @@ export interface PhillipsErollOptions {
|
|
|
45
45
|
/**
|
|
46
46
|
* Where on the paper the roll had run after so many seconds. The
|
|
47
47
|
* default is the constant speed the system states, which ignores
|
|
48
|
-
* the take-up spool; pass `paperAt` of welte-
|
|
48
|
+
* the take-up spool; pass `paperAt` of welte-mignon-emulator to
|
|
49
49
|
* account for it.
|
|
50
50
|
*/
|
|
51
51
|
placeAt?: (elapsed: Seconds) => Millimeters;
|
|
@@ -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;
|
|
@@ -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
|
},
|