npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

welte-mignon-emulator

v1.1.0

Published

The expression mechanism of a Welte-Mignon: the take-up spool, the nuancing bellows and the pedals, with the latching relay of the T-100 and the duration-coded relay of the T-98 in front of them.

Readme

Welte-Mignon Emulator

A few red Welte rolls of the late production period carry drawn expression lines (Nuancierlinien). According to Hans-W. Schmitz they were drawn onto the finished rolls by two pens coupled to the two Nuancierbälge while the roll was played back („Welte-Mignon und Hupfeld DEA", Das Mechanische Musikinstrument 19, 1981, pp. 4–5). Hermann Gottschewski's proposal at the 3rd Global Piano Roll Meeting 2024 was to read the rules of the mechanism off such lines and then emulate the lines by software. He observed several differences from common emulators such as SUPRA's midi2exp, for example that the Welte crescendos and diminuendos are far from linear. This emulator follows Gottschewski's proposal.

It derives the travel of the two Nuancierbälge from the punched expression code by modelling the pneumatics that move them: the valves the code operates, the conduits of different bore through which the bellows fills and empties, the wind chamber they draw on, and the Mezzoforte stop that arrests the travel. Their constants were fitted by differential evolution with a Nelder–Mead polish against the expression lines of six rolls, and scored on the blocks of each roll left out of the fit. The library ships the six different instrument settings as presets, named by Welte number, and beside them a consensus: one instrument fitted to all six lines at once. The consensus is the default.

This emulator also implements Gottschewski's model of roll acceleration (Die Interpretation als Kunstwerk, Laaber 1996, pp. 135–137) and makes an assumption about the pedals' travel based on the pneumatic description by Peter Hagmann (Das Welte-Mignon-Klavier, die Welte-Philharmonie-Orgel und die Anfänge der Reproduktion von Musik, Diss. Basel, Bern 1984, pp. 106–107).

Using it as a library

Node 24 or later, no runtime dependencies. The package is welte-mignon-emulator on npm, with three entry points: . for the nuancing block both scales share, ./t100 for the red Welte's latching relay and the instruments it was fitted as, and ./t98 for the green Welte's duration-coded relay. Hagmann has the nuancing unit built the same on both scales (p. 96), so it is written once and the two relays stand in front of it.

npm install          # typescript and @types/node, for checking only
npm run check
npm test
npm run build        # writes dist/, which is what the package exports

dist/ is committed, so a dependency on a checkout of this repository needs no build of its own, which is how linked-rolls and the empirics are developed against it:

"welte-mignon-emulator": "file:../welte-t100"

Rebuild dist/ whenever src/ changes, and commit it. The empirics import the built package, so an experiment on the model is an edit here, npm run build, and a run there. src/t100/instruments.data.ts is generated by the empirics' src/cli/instruments.ts from its fits and written into this tree; the same build and commit follow.

Releases go out through .github/workflows/publish.yml: raise the version in package.json, commit, and push a tag v<version>, or run the workflow from the Actions tab.

Example

A SUPRA raw MIDI carries one tick per scan row and the roll's metadata. The emulator reads its bytes, sets the time axis from the spool law, turns the expression perforations into the open area of each tracker port row by row, and runs the two mechanisms on that grid. Nothing in the library reads a file itself.

import { readFileSync } from "node:fs";
import {
  aperturePorts, Grid, instrumentParameters, perforations, pneumaticModel, readRoll,
  runPedals, travelBetweenRails, type Half,
} from "welte-mignon-emulator/t100";

// The punched code as the open area of each tracker port, row by row, on the spool's time axis.
const roll = readRoll("jq774vx6544", readFileSync("jq774vx6544_raw.mid"));
const punches = perforations(roll);
const lastRow = Math.max(...punches.map((punch) => punch.rowOff));
const grid = Grid.overRows(roll.timing, roll.timing.rowAtTick(0), lastRow);
const ports = aperturePorts(grid, punches);

// Each half has its own bellows and its own constants: the consensus instrument by default,
// or { preset: "3309" } for the setting that drew one particular roll.
const travelOf = (half: Half) => {
  const params = instrumentParameters(half);
  return travelBetweenRails(pneumaticModel.run({ grid, half, ports }, params), params);
};

// Bellows travel per row: 0 fully open (P.P.), 1 fully closed (F.F.).
const nuance = {
  bass: travelOf("bass"),
  treble: travelOf("treble"),
};

// Damper and hammer rail per row, 0 at rest to 1 fully moved. The default has the dampers
// arrive in 180 ms; `pedalBrushing` slows the fall until the quick runs of the SUPRA corpus
// dip without damping, the other reading of those runs (see `src/core/pedal.ts`).
const pedals = runPedals({ grid, ports });

A roll that is not a SUPRA scan needs only its punches in scan rows and a Grid of seconds, which is how linked-rolls drives the emulator from an edition. geometryInMm puts a roll of another punch diameter on the same grid, and levelChanges turns the pedal travel into controller messages.

Layout

| | | | --- | --- | | src/index.ts | the scale-neutral surface, . | | src/core/ | what both scales share: the take-up spool, the tracker-bar aperture, the relay valves, the flow law, the Mezzoforte stop, the nuancing bellows, the pedal mechanism, a MIDI reader and writer | | src/t100/ | the red Welte, ./t100: the expression code, the latching relay, the SUPRA reader, and the instruments playback runs as; instruments.data.ts is generated from the fits | | src/t98/ | the green Welte, ./t98: the expression code, the duration-coded relay, the rewind, and the instruments | | docs/sources.md | what the sources say, by topic: Hagmann, Schmitz, Gottschewski, the patents, midi2exp and pianolatron |

The model

The state is the closure of the Nuancierbalg: 0 fully open, which sets the cone valve for the least vacuum and so the softest attack, 1 fully closed and loudest. Valves admit air to it or draw air out of it through conduits of different bore, and the speed of a bellows filling through a conduit depends on how far it still has to go:

dx/dt = Σ  g · a · sign(T − x) · |T − x|^α

one term per open path, with g the conductance of its conduit, a how far the tracker port is open, T the position that path pulls towards, and α the exponent of the flow law. α = 1 is a laminar throttle and gives an exponential approach, α = ½ an orifice, α = 0 a constant rate (which is what midi2exp and pianolatron assume). α is fitted rather than chosen, so the family contains the prior art as a special case. The fitted values are in src/t100/instruments.data.ts.

The two relays

The equation and everything after it are the same on both tracker scales, on Hagmann's authority that the nuancing unit is "von geringfügigen Unterschieden abgesehen, für beide Blockskalen gleich konstruiert" (p. 96) and because his Anhang 13 and 14 are the same drawing of parts 85–101 under two different relays. What differs is which paths are open, and when.

The T-100 puts a hold chamber above each "on" valve, so a short punch sets a function and it stands until its cancel line is read. That is claim 3 of DRP 162 708, whose purpose is stated as "daß man im Notenblatt nur kurze Öffnungen vorzusehen braucht, um lang anhaltende Wirkungen hervorzubringen". Three paths hang on it: conduit 39 switched between vacuum and atmosphere, conduit 23 under the sforzando, throttle 96 under the cancel.

The T-98 has no hold chamber on any of its four units, so "die Funktion bleibt genau so lange ausgeführt, als die entsprechende Perforation im Notenband über die Gleitblock-Oeffnung läuft" (pp. 100 f.). Four paths, and the fourth is the difference that matters: bore 100 is a permanently open bleed to atmosphere, which Welte added because the crescendo's own throttle cannot readmit air fast enough (Betriebsanleitung p. 13). It weakens the crescendo, so the ceiling a slow crescendo reaches is the balance of the two screws rather than a fitted asymptote, and the symmetry Welte requires of the crescendo and its decay falls out of the structure instead of having to be imposed. Four conduits stand on one bellows and nothing arbitrates between them, so opposed drives add as flows and the bellows goes where they balance — neither midi2exp's arithmetic cancellation of constant velocity steps nor PlaySK's forte-wins. A long perforation on the bass sforzando-piano line is the rewind, read a second time by a sluggish dead-banded integrator on the same valve (Skala-Rolle 98 §10).

Nothing on the T-98 side is fitted. src/t98/instruments.ts carries a discriminated union of genuine instruments, to be fitted to drawn green nuance lines, derived ones, to be fitted so that a green code reproduces what the T-100 emulator makes of the red copy of the same recording, and the unfitted starting values that ship in the meantime. The first two are empty. The difference between a genuine and a derived instrument, in the printed ordinate both scales share, is how far the transfer of a red reading onto the green mechanism succeeded, and it is what the two exist for. Welte's own regulation controls run as tests in src/t98/controls.test.ts and are a gate on any fitted result rather than a term in its objective; several of them record what the starting values do against what Welte requires.

Observations

  • Differently regulated instruments. Each of the six rolls fits about as well as 3309, with held-out errors between 0.017 and 0.053 of the scale, and a foreign roll's constants score two to three times worse than the roll's own. Re-registering the rails and the lead removes about a tenth of the excess.
  • Bass sits below treble on all six rolls. Both the crescendo and the sforzando asymptotes are lower in the bass than in the treble on every roll. Under a sign test that is p = 0.03 each, uncorrected, with nine parameters examined, so it is suggestive at most. It points the same way as Hagemann's measurement that the discant dome travels further than the bass dome for the same change of vacuum (Das Mechanische Musikinstrument 80, 2001, p. 26).
  • Fall is four times the rise. The sforzando release runs at about four times the rate of the sforzando closing, on 51 and 81 clean episodes each way. Welte's own control 6c requires the two increments equal, so the instruments that drew the lines did not satisfy it. Against the line, midi2exp's 300 ms fast crescendo is of the right order and its 400 ms fast decrescendo is six to seven times too slow.
  • Crescendo latch speeds the sforzando up. With the crescendo set, a sforzando closes faster by about 1.1 units/s. Conduit 39 admitting air whenever the crescendo is cancelled predicts about two thirds of that gap, and the rest grows with position. The model carries the remainder as a load the nuancing system puts on its own blower, which is a proposal rather than a reading, and the crescendo-off episodes it rests on are few.
  • Mezzoforte stop rebounds. Arrivals at the hook faster than 5 units/s overshoot and swing back once, a damped bounce of period about 32 ms that has settled by 50 ms. The rails show nothing of the kind. Rests arriving from below lie above the rests arriving from above, the opposite of the rigid hook of finite thickness Schmitz draws, and what a stop that yields in the direction it is pushed would give. The rising rests are 13 and 7.
  • Lead of line drifts along the roll. Gottschewski observed that the lines run ahead of the punches. Along the roll the lead drifts by up to a few rows per 100 000 rows of paper, in either direction, as often one way as the other, and often by different amounts in the two halves.