linked-rolls 0.20.0 → 0.22.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 +11 -1
- package/lib/RollCopy.js +25 -18
- package/lib/TrackerBar.d.ts +65 -2
- package/lib/TrackerBar.js +38 -6
- package/lib/alignment.js +3 -2
- package/lib/constraints.js +4 -2
- 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,11 +320,21 @@ 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
|
/**
|
|
326
334
|
* The tracks a copy carries holes on that the tracker bar does not read.
|
|
327
|
-
* A non-empty result usually means the scan is calibrated wrongly.
|
|
335
|
+
* A non-empty result usually means the scan is calibrated wrongly. A hole
|
|
336
|
+
* lying across several positions is counted against each one the bar
|
|
337
|
+
* cannot read, and not at all where it reads them all.
|
|
328
338
|
*/
|
|
329
339
|
export declare function unreadTracks(features: AnyFeature[], bar: TrackerBar): Map<Track, number>;
|
|
330
340
|
/**
|
package/lib/RollCopy.js
CHANGED
|
@@ -26,34 +26,41 @@ 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
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)
|
|
43
|
+
// An opening across two positions uncovers both bar holes and so
|
|
44
|
+
// reads as both commands; one on a single position gives the one.
|
|
45
|
+
.flatMap((feature) => bar.meaningsOf(feature.vertical).map(meaning => ({
|
|
46
|
+
id: `symbol_${v4()}`,
|
|
47
|
+
...meaning,
|
|
48
|
+
carriers: [assignReference(feature.id)]
|
|
49
|
+
})));
|
|
43
50
|
}
|
|
44
51
|
/**
|
|
45
52
|
* The tracks a copy carries holes on that the tracker bar does not read.
|
|
46
|
-
* A non-empty result usually means the scan is calibrated wrongly.
|
|
53
|
+
* A non-empty result usually means the scan is calibrated wrongly. A hole
|
|
54
|
+
* lying across several positions is counted against each one the bar
|
|
55
|
+
* cannot read, and not at all where it reads them all.
|
|
47
56
|
*/
|
|
48
57
|
export function unreadTracks(features, bar) {
|
|
49
58
|
const counts = new Map();
|
|
50
59
|
features
|
|
51
60
|
.filter(feature => feature.type === 'Hole')
|
|
52
|
-
.
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
counts.set(position, (counts.get(position) || 0) + 1);
|
|
56
|
-
});
|
|
61
|
+
.flatMap(feature => bar.positionsIn(feature.vertical))
|
|
62
|
+
.filter(position => !bar.meaningOf(position))
|
|
63
|
+
.forEach(position => counts.set(position, (counts.get(position) || 0) + 1));
|
|
57
64
|
return counts;
|
|
58
65
|
}
|
|
59
66
|
/**
|
package/lib/TrackerBar.d.ts
CHANGED
|
@@ -56,8 +56,31 @@ export interface TrackerBar {
|
|
|
56
56
|
* of their own, as the Licensee's do, states none.
|
|
57
57
|
*/
|
|
58
58
|
readonly paperSpeed?: SpeedMeasure;
|
|
59
|
-
/**
|
|
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;
|
|
70
|
+
/**
|
|
71
|
+
* `undefined` for a position the bar does not read. A measured place
|
|
72
|
+
* is snapped to the nearest position: perforations sit on the grid and
|
|
73
|
+
* measurements of them scatter around it.
|
|
74
|
+
*/
|
|
60
75
|
meaningOf(position: Track): TrackMeaning | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* What the bar reads off a feature, which may lie across more than one
|
|
78
|
+
* position. A perforation lifts every valve whose bar hole it uncovers,
|
|
79
|
+
* so an opening across two positions reads as two commands; one on a
|
|
80
|
+
* single position reads as the one `meaningOf` gives, and one on
|
|
81
|
+
* positions the bar does not read as none at all.
|
|
82
|
+
*/
|
|
83
|
+
meaningsOf(span: OnBar): readonly TrackMeaning[];
|
|
61
84
|
/**
|
|
62
85
|
* `meaningOf` inverted: the position this bar reads the meaning on,
|
|
63
86
|
* or `undefined` where it does not read it at all. A symbol's track
|
|
@@ -65,8 +88,10 @@ export interface TrackerBar {
|
|
|
65
88
|
* sits on exactly one position of a given bar.
|
|
66
89
|
*/
|
|
67
90
|
positionOf(meaning: TrackMeaning): Track | undefined;
|
|
68
|
-
/** `undefined` for a position the bar does not read. */
|
|
91
|
+
/** `undefined` for a position the bar does not read; snapped as `meaningOf` is. */
|
|
69
92
|
roleOf(position: Track): TrackRole | undefined;
|
|
93
|
+
/** The positions a feature lies across, snapped to the grid. */
|
|
94
|
+
positionsIn(span: OnBar): readonly Track[];
|
|
70
95
|
}
|
|
71
96
|
/**
|
|
72
97
|
* What a position says, as a key. Two bars read the same thing exactly
|
|
@@ -85,6 +110,37 @@ export declare const systemIdOf: (system: Concept | undefined) => string | undef
|
|
|
85
110
|
* in the bar's own 1-based numbering; `describeTrackerBar` gives them
|
|
86
111
|
* their type.
|
|
87
112
|
*/
|
|
113
|
+
/**
|
|
114
|
+
* Where a roll's own content ends, as far as its perforations say.
|
|
115
|
+
*
|
|
116
|
+
* This is a question about the paper, not about the mechanism: it asks where
|
|
117
|
+
* the rewind is *punched*, not when the rewind pneumatic takes hold. The second
|
|
118
|
+
* is a matter of valve lift and belongs to whatever performs the roll. Keeping
|
|
119
|
+
* them apart is what lets collation and counting ask this without an emulator.
|
|
120
|
+
*/
|
|
121
|
+
export type RollEnd = {
|
|
122
|
+
readonly at: Millimeters;
|
|
123
|
+
readonly because: 'rewind';
|
|
124
|
+
};
|
|
125
|
+
/** Anything carrying a place on the roll and a position on the bar. */
|
|
126
|
+
export type PlacedOnBar = {
|
|
127
|
+
readonly horizontal: {
|
|
128
|
+
readonly from: Millimeters;
|
|
129
|
+
readonly to: Millimeters;
|
|
130
|
+
};
|
|
131
|
+
readonly vertical: {
|
|
132
|
+
readonly from: Track;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Where a feature lies across the bar: one place, or a run of them where
|
|
137
|
+
* `to` is given. Structural, so a feature's `vertical` passes as it is and
|
|
138
|
+
* this module need know nothing about features.
|
|
139
|
+
*/
|
|
140
|
+
export type OnBar = {
|
|
141
|
+
readonly from: Track;
|
|
142
|
+
readonly to?: Track;
|
|
143
|
+
};
|
|
88
144
|
export interface TrackerBarSpec {
|
|
89
145
|
id: string;
|
|
90
146
|
name: string;
|
|
@@ -107,6 +163,13 @@ export interface TrackerBarSpec {
|
|
|
107
163
|
rewindTrack?: number;
|
|
108
164
|
/** The speed the system runs its rolls at, where the literature states one. */
|
|
109
165
|
paperSpeed?: SpeedMeasure;
|
|
166
|
+
/**
|
|
167
|
+
* How long a perforation on a *shared* rewind position has to be before it
|
|
168
|
+
* is the rewind rather than the command the position usually carries. Only
|
|
169
|
+
* meaningful with `rewindTrack`: a system that gives the rewind a line of
|
|
170
|
+
* its own needs no threshold, since anything there is the rewind.
|
|
171
|
+
*/
|
|
172
|
+
rewindHold?: Millimeters;
|
|
110
173
|
}
|
|
111
174
|
export declare const describeTrackerBar: (spec: TrackerBarSpec) => TrackerBar;
|
|
112
175
|
/**
|
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
|
|
@@ -20,24 +20,42 @@ const areasOf = ({ notes, trackCount }) => [
|
|
|
20
20
|
{ role: 'treble-expression', from: track(notes.to + 1), to: track(trackCount) }
|
|
21
21
|
];
|
|
22
22
|
const scopeOf = (role) => role === 'bass-expression' ? 'bass' : 'treble';
|
|
23
|
+
/**
|
|
24
|
+
* The bar position a measured place falls on. Places are measured off a
|
|
25
|
+
* scan and scatter around the grid, while the bar has holes only at whole
|
|
26
|
+
* positions, so the nearest one is the one uncovered.
|
|
27
|
+
*/
|
|
28
|
+
const snap = (place) => track(Math.round(place));
|
|
29
|
+
/** The positions a span reaches, both ends snapped and the run between them. */
|
|
30
|
+
const positionsBetween = (span) => {
|
|
31
|
+
const ends = [snap(span.from), snap(span.to ?? span.from)];
|
|
32
|
+
const [first, last] = [Math.min(...ends), Math.max(...ends)];
|
|
33
|
+
return Array.from({ length: last - first + 1 }, (_, step) => track(first + step));
|
|
34
|
+
};
|
|
23
35
|
export const describeTrackerBar = (spec) => {
|
|
24
36
|
const areas = areasOf(spec);
|
|
25
|
-
const
|
|
37
|
+
const areaAt = (position) => areas.find(area => position >= area.from && position <= area.to)?.role;
|
|
38
|
+
const roleOf = (position) => areaAt(snap(position));
|
|
26
39
|
const meaningOf = (position) => {
|
|
27
|
-
const
|
|
40
|
+
const place = snap(position);
|
|
41
|
+
const role = areaAt(place);
|
|
28
42
|
if (!role)
|
|
29
43
|
return undefined;
|
|
30
44
|
if (role === 'note') {
|
|
31
45
|
return {
|
|
32
46
|
type: 'note',
|
|
33
|
-
pitch:
|
|
47
|
+
pitch: place - spec.notes.from + spec.notes.lowestPitch
|
|
34
48
|
};
|
|
35
49
|
}
|
|
36
|
-
const expressionType = spec.expressions.get(
|
|
50
|
+
const expressionType = spec.expressions.get(place);
|
|
37
51
|
if (!expressionType)
|
|
38
52
|
return undefined;
|
|
39
53
|
return { type: 'expression', expressionType, scope: scopeOf(role) };
|
|
40
54
|
};
|
|
55
|
+
const meaningsOf = (span) => positionsBetween(span).flatMap(position => {
|
|
56
|
+
const meaning = meaningOf(position);
|
|
57
|
+
return meaning ? [meaning] : [];
|
|
58
|
+
});
|
|
41
59
|
const positions = new Map(Array.from({ length: spec.trackCount }, (_, i) => track(i + 1))
|
|
42
60
|
.flatMap(position => {
|
|
43
61
|
const meaning = meaningOf(position);
|
|
@@ -48,18 +66,32 @@ export const describeTrackerBar = (spec) => {
|
|
|
48
66
|
if (rewind === undefined) {
|
|
49
67
|
throw new Error(`${spec.name} declares no rewind track`);
|
|
50
68
|
}
|
|
69
|
+
// A rewind on a line of its own is unambiguous; one sharing a line is only
|
|
70
|
+
// the rewind when it is far longer than that line's usual command.
|
|
71
|
+
const shared = spec.rewindTrack !== undefined;
|
|
72
|
+
const endsAt = (features) => {
|
|
73
|
+
const hold = spec.rewindHold ?? mm(0);
|
|
74
|
+
const candidates = features
|
|
75
|
+
.filter(feature => snap(feature.vertical.from) === rewind)
|
|
76
|
+
.filter(feature => !shared || feature.horizontal.to - feature.horizontal.from >= hold)
|
|
77
|
+
.map(feature => feature.horizontal.from);
|
|
78
|
+
return candidates.length ? { at: mm(Math.min(...candidates)), because: 'rewind' } : undefined;
|
|
79
|
+
};
|
|
51
80
|
return {
|
|
52
81
|
id: spec.id,
|
|
53
82
|
name: spec.name,
|
|
54
83
|
width: spec.width,
|
|
55
84
|
trackCount: spec.trackCount,
|
|
85
|
+
endsAt,
|
|
56
86
|
areas,
|
|
57
87
|
expressionTypes: [...new Set(spec.expressions.values())],
|
|
58
88
|
rewindTrack: track(rewind),
|
|
59
89
|
...(spec.paperSpeed && { paperSpeed: spec.paperSpeed }),
|
|
60
90
|
meaningOf,
|
|
91
|
+
meaningsOf,
|
|
61
92
|
positionOf: meaning => positions.get(keyOf(meaning)),
|
|
62
|
-
roleOf
|
|
93
|
+
roleOf,
|
|
94
|
+
positionsIn: positionsBetween
|
|
63
95
|
};
|
|
64
96
|
};
|
|
65
97
|
/**
|
package/lib/alignment.js
CHANGED
|
@@ -59,8 +59,9 @@ const noteOnsets = (features, bar) => features
|
|
|
59
59
|
.flatMap((feature) => {
|
|
60
60
|
if (feature.type !== 'Hole')
|
|
61
61
|
return [];
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
return bar.meaningsOf(feature.vertical)
|
|
63
|
+
.filter(meaning => meaning.type === 'note')
|
|
64
|
+
.map(meaning => ({ pitch: meaning.pitch, at: feature.horizontal.from }));
|
|
64
65
|
})
|
|
65
66
|
.sort(byPlace);
|
|
66
67
|
const median = (values) => {
|
package/lib/constraints.js
CHANGED
|
@@ -79,8 +79,10 @@ const carriersOffTheirMeaning = (view, version, perforations) => {
|
|
|
79
79
|
const copy = view.copyOf(carrier.id);
|
|
80
80
|
if (!copy)
|
|
81
81
|
return false;
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// A carrier lying across several positions reads as several commands,
|
|
83
|
+
// and carries the symbol as long as one of them is the symbol's.
|
|
84
|
+
return !barOf(copy).meaningsOf(carrier.vertical)
|
|
85
|
+
.some(meaning => keyOf(meaning) === keyOf(symbol));
|
|
84
86
|
};
|
|
85
87
|
return perforations
|
|
86
88
|
.filter(symbol => view.carriersOf(symbol).some(carrier => misread(carrier, symbol)))
|
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
|
+
};
|