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

linked-rolls

v0.53.0

Published

Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system

Readme

linked-rolls

linked-rolls is a lightweight Typescript library for creating, manipulating and exporting digital editions of piano roll. In particular it allows to:

  • import piano rolls from different formats (such as SUPRA's roll analysis files, DSP, etc.)
  • collate differing piano roll copies
  • create an manipulate editorial assumptions
  • export the edition as JSON-LD (based on the Roll Edition Format)

This library is the foundation of the Roll Desk.

Vocabulary

The JSON-LD export uses the Roll Edition Ontology (REO), namespace https://w3id.org/reo/, prefix reo, alongside CIDOC CRM, LRMoo and CRMinf. The ontology lives in ontology/, and the context every export references is https://w3id.org/reo/context.jsonld. The ontology, its type vocabulary and the context are published at https://pfefferniels.github.io/linked-rolls/reo/; the w3id.org identifiers are not registered yet. ontology/README.md records the naming decisions.

Beliefs travel as JSON-LD-star annotations (@annotation), which state the triple they annotate. A reference the edition holds possible, unlikely or false is therefore written as an embedded node beside the document: RDF names the statement without stating it, and importJsonLd puts it back where it stood. A doubted value, such as a date or an attribution, stays annotated in place for now. Reading the beliefs needs a processor that implements JSON-LD-star, such as the Ruby json-ld gem; jsonld.js ignores @annotation, and every belief with it.

Format revisions

Files written by linked-rolls 0.1 load unchanged: importJsonLd recognises their shapes and brings them to the current format, in which versions and conditions carry a typology key beside their type, the keeper and the production metadata are nodes with a name and authority links, and the roll names its reproducing system. A date written as a value of its own is read as the day the event falls within. A copy that held its features in one list, naming by id what a modification had added, has each feature put into the act that brought it about, and a feature a patch bears that was left unnamed is named after the patch. Exports are always in the current format.

How a date is stated

A date is the time-span of the event rather than a value on it. within gives the day it falls within, and after and before give the bounds where nobody can give the day. A bound the edition does not know is left out, so after alone says "not before". In RDF the three become P82 at some time within, P82a begin of the begin and P82b end of the end on the E52 Time-Span the event has. assignDate, notBefore and notAfter build the three shapes, and dateOf, earliestOf and latestOf read them.

Note that before and after mean something else on a command, where they order two onsets. The context defines them for a date within the date itself, so the two never meet.

What a version derives from

A version names the versions it is held to derive from in basedOn, each under the belief it rests on. Its text is read against the principal derivation, the first of those held most certain, and one held unlikely or false never gives the text. The others stand as hypotheses, such as a contamination: stateDerivation adds one and clearDerivation takes it back. A version may leave out its edits where nobody can state them, as for a text that only a recording hints at. It then reads as the version it derives from, and reservationsAboutVersion says so. A file that names a single derivation loads as a list of one.

What a version is called is not stored with it. siglaOf reads the sigla off the stemma as it stands, so a label never outlives the arrangement it describes, and nothing should cite one without saying which state of the edition it belongs to.

Handed an EditionView, which knows the copies as well, siglaOf lowercases the versions no copy's features carry at first hand, as editions mark a state nothing surviving shows: r3 stands to R3 as a reconstructed state stands to a witnessed one. A version reached only through the versions derived from it is lowercased, and so is one a copy does no more than state it carries, since a statement gives no readings. attestedVersions reports the same fact on its own. Handed the versions alone, siglaOf cannot tell and leaves every siglum in capitals.

The tolerance a derivation was collated at

collationTolerance on a derivation is the window two readings of one symbol must fall in to be collated: how far apart they may lie at either end, and, since two copies differ by a systematic offset as well as by scatter, where that window is centred. A window naming no offset is centred on nothing, which is what every window written before the offset was held here means.

The number decides what counts as a reading at all, so it is worth measuring rather than picking. scatterOfCopy measures how far one copy puts each symbol from where the copies collated with it put it, and scatterOf describes that sample: its median, its scatter as a median absolute deviation scaled to a standard deviation, a histogram with the normal curve to lay over it, and the readings the curve does not account for. The scatter is taken from the median because the displacements an editor is looking for are in the sample, and a standard deviation would grow towards them until they no longer stood out.

The copies named are one side of the comparison and the rest are the other, so which copies to name is the first question and the edition no longer answers it: a collation hands a child's carriers up to the parent's symbols, so a descendant's copies come to bear an ancestor's readings and look like its own. sidesOf reads the sides off the edits instead, the copies bearing what a version inserts standing for the reading it moves to and those bearing what it strikes for the reading it moves from. Naming a set that is not a side measures one copy's noise against a mixture of both sides, which is wider than the truth.

The tolerance that follows is the scatter times a threshold fixed by the size of the sample, the point beyond which fewer than one reading is expected to fall by chance (departureThreshold). For the few hundred readings an edge of a stemma yields this lands a little above three. toleranceAcross covers several samples with one window, since a derivation states one tolerance while notes and expressions scatter differently. It asks which side was measured, because a window is signed: the offset runs from the parent to the child, and one stored the other way round has the right width and the wrong centre, so it separates readings that belong together and merges readings that do not while nothing in the number looks wrong. inferredTolerance builds the belief the number rests on, to annotate collationTolerance with. It is held likely and not true: the tolerance follows from the sample only as far as the sample is normal, which is what the reported excess kurtosis and tail counts are there to show.

Two limits are worth naming. Notes and expressions are estimated apart by default, which is a stopgap standing in for a skew across the width of the paper rather than a distinction in the model, so the grouping is a parameter of the estimator and appears nowhere in the format. And the method screens: it says which readings the curve does not account for, and an editor says which of those are editorial acts.

A separation reports which end made it (separatedBy, and admittedAtEnds for a collation). The two ends answer different questions: the onset, with the kind, decides whether two copies read one command, and the end decides whether that command was lengthened or shortened. On welte225.org the end alone decides 24 collations of 2839 and the onset alone 39, so neither test is idle, but a difference at the end is as often a punch measured badly as a punch genuinely prolonged, and the two are not worth confusing.

What a reader adds to a hole

A copy read pneumatically rather than scanned reports how long a valve stayed open, and a valve is held on past the perforation that opened it, so its holes run longer than the punched slots while their onsets agree. Left in, that is not a difference between copies: a collation comparing such a copy's hole ends against a scanned copy's compares an on-time with a slot, and reads the one as a lengthening of the other.

shortenHoles takes the extension off and records it on the copy, revertShortening puts it back, and readFromPhillipsEroll will take it off at import. Only holes are touched, what a writing spans being no valve. The figure is measured against the other copies rather than derived from the bar, since Phillips gives only the difference between two bars and not the absolute.

On welte225.org that figure is about 1.6 mm, and its shape is simple. It does not grow with the length of the perforation: the ratio of the read length to the true one falls from 1.34 on the shortest punches to 1.01 on the longest, which is what a constant added to everything does. There is no shortest on-time the valve cannot fall below. It shows no run along the roll, though one roll's span of paper speed is too narrow to tell a constant in millimetres from a constant in time. What it does vary with is the port that read it, by about half a millimetre either way and with no gradient across the bar, which is measured but too coarse to model from a single roll.

Collating a derivation a second time

A collated symbol is one symbol carrying every copy that reads it, so a collation cannot simply be run again: there is no second symbol left to match. separateReadings is the inverse. It takes the carriers of one side back off the symbols it shares, gives them to new symbols of the version's own, and states the exchange, leaving the edition in the state a collation would have produced had nothing joined. connectVersions at a new tolerance then re-collates, joining again whatever the new window admits.

It takes a side and not a copy, the same side sidesOf names and a window is measured over. Naming one copy of a side that has several does not separate that side: the rest of it stays behind on the other side's symbol, and a collation then compares one copy against that mixture rather than the two texts against each other.

Nothing an editor established is lost by that. connectVersions rewrites only what a collation wrote (isCollationsOwn): a bare insertion or deletion, or an equivalence between two systems' spellings. An edit naming what the change is, why it was made, or what it rests on stays, and the symbols it speaks for are left out of the collation. An equivalence counts as the collation's own, since it is derived from the two systems' vocabularies rather than read off the paper and freezing it would leave the transfers uncollatable, but one drawn a second time over the very same symbols is kept as it stands, identifier and motivation and all. Readings already separated by hand are carried by one copy alone, so separateReadings passes over them and they keep their identifiers.

What brought a feature about

A copy states no list of features. Each feature stands in the act that brought it about: the copy's own production for what the punching made, an Alteration for what a later hand made, an Attachment for a patch glued on. A Removal names by id what was taken off, which stood there before the act.

Which act a feature stands in says when it came about and by whose hand, and says nothing about what kind of feature it is. A hole may be punched by hand long after the roll was cut, and a mark may come from the factory, rarely though that happens. A feature no later act is known to have made belongs to the punching, which is where a reading of a scan puts every hole it finds.

Three kinds of act rather than two, because the CRM tells them apart. E79 Part Addition asks that what is added be "a separate identifiable whole prior to" the act, which a glued-on label is and a pencil line is not: drawing, writing and punching bring a feature into being and are productions (E12), which state what they made with P108 has produced.

The export states what that shape only implies. A copy bears every feature its acts brought about (P56 bears feature) and is composed of the patches glued onto it (P46 is composed of, a patch being an object rather than a feature); an attachment augments the copy and a removal diminishes it. The four keys are derived on the way out and read off again on the way in, so they are no part of the edition, and they are the one place where the JSON tree and the graph differ in shape.

Where a copy's features come from

readFrom states what a copy's features were read from: the roll itself, a scan of it, an analysis somebody else measured on a scan, a reading of the timed switches of a roll reader, an emulation in which the roll has already been read into notes and commands, or a recording of the copy being played on an instrument. Beside the kind it holds who carried the capture out, on what device, with what software, on which instrument a recorded copy was played, when, and a note. What is not known is left out.

This says where the numbers of the edition come from and nothing about the state of the paper, which is a condition of the copy. In RDF it is reo:capture, an activity typed by what it read from; the file it produced is crmdig:L11 had output, the machine it ran on crmdig:L12 happened on device, the software crmdig:L23 used software or firmware, the instrument crm:P16 used specific object.

Nothing records that a copy is doubtful. reservationsAbout works out from what a copy states what the edition cannot vouch for in it: that it names no source, that the making of its source is undocumented, that no software or instrument is named for an emulation or a recording, that its features are somebody else's reading, that its source bears no physical evidence, that no measuring software is recorded, that it is not calibrated, that nobody is known to hold it. A reservation goes away when the gap it names is filled.

A copy nobody can reach, such as one known only from a recording, has no features to carry the symbols of a version. It states instead which versions it is held to carry, in carries, each under a belief that says how certainly and why. witnessesOf gathers the copies of a version, whether their features carry its symbols or they state that they carry it, a statement with the belief it rests on; versionsWitnessedBy gathers the same from the side of a copy. carriageProblems reports a statement made beside features that carry symbols already, or one naming a version the edition lacks. A copy may carry a siglum, which is given by hand and not read off anything; one without is named by its keeper.

import { reservationsAbout, stateSource } from 'linked-rolls'

const next = produce(edition, stateSource(copyId, {
    kind: 'emulation',
    output: 'https://example.org/wm225.mid',
    note: 'MIDI from a third party; the emulator is not named.'
}))

reservationsAbout(next.copies[0]).map(reservation => reservation.note)

Emulation

Emulation turns a version of the edition into MIDI. The core of the library does the part that belongs to the edition: it negotiates the symbols of a version into placed events, hands them to a ReproducingSystem, and writes the performance out with every note and pedal step labelled by the symbol it performs. A reproducing system is a tracker bar and a perform function; the core does not depend on any one instrument's model.

The first system is the red Welte, linked-rolls/welte-t100, built on welte-mignon-emulator: the take-up spool sets the time axis, the Nuancierbälge fill through their conduits and are arrested by the Mezzoforte pin, and the two pedals travel rather than switch. The nuancing constants come as instruments: the consensus fitted across the hand-drawn nuance lines of six rolls, which is the default, and the setting that drew each of those rolls, named by its Welte number, all with the terms that describe the drawing apparatus switched off. The pedals leave no drawn line, so their constants come as two readings, pedalPresets.damping, under which every lift the rolls punch damps, and pedalPresets.brushing, under which the quick runs of latch changes brush the strings without damping. What the emulator does not determine is how bellows travel maps onto MIDI velocity; WelteT100Options.velocity anchors that map at the open rail, the Mezzoforte pin and the closed rail, and its defaults are midi2exp's.

import { Emulation } from 'linked-rolls'
import { welteT100System } from 'linked-rolls/welte-t100'

const emulation = new Emulation(welteT100System)
emulation.emulateVersion(version, view)
const midi = emulation.asMIDI()

The second is the green Welte, linked-rolls/welte-t98. Everything downstream of the relay is the same mechanism — Hagmann has the nuancing unit built the same for both tracker scales (p. 96) — and what differs is in front of it: each function lasts exactly as long as its own perforation runs over the glide block rather than latching until a cancel line is read, four conduits stand on one bellows and their drives add as flows, the crescendo's ceiling is the balance of its throttle against a permanently open bleed rather than a cap, the two pedals sit on the opposite edges of the paper, and a long perforation on the bass sforzando-piano line sends the roll back. Its constants are not fitted: instruments.genuine and instruments.derived are empty until their fits are run, and until then a playback runs on unfitted starting values, which every curve says in its own instrument field.

Both systems share one velocity map, and DynamicsCurve.travel means the same thing on both: the position on the printed ordinate of Welte's own ruled band, 0 at that half's P.P. gridline and 1 at the shared F.F. line. That is what lets a red issue and a green issue of one recording be compared at all, since the band is ruled the same way on both.

import { welteT98System } from 'linked-rolls/welte-t98'

The emulator is an optional peer dependency: an application that uses one of the Welte systems installs welte-mignon-emulator itself, and one that only reads editions does not need it. For development on both at once, npm link a checkout of welte-t100 into this repository.

Alignment, order and pairing

A command takes its place from the holes that carry it. Four fields on a command let an editor state where the measurement should give way. alignedWith names another command whose onset this one takes in the performance, as a "crescendo off" is meant to fall on the note it belongs to. before and after name a command whose onset this one precedes or follows, without saying by how much: where the copies disagree on which side of a note an expression falls, the statement settles the order. A command the measurement already has on the stated side keeps its place. One it does not is put on that side, as far from the reference as the copies that agree with the statement put it, and a punch diameter away where none does. A command makes one of these three statements at most. pairedWith names a partner whose distance to this one is fixed, as a "forzando on" belongs with its "forzando off": whatever displaces the one displaces the other. Any two commands may be paired, the relation is symmetric, and it is stated on one side only. All are applied when a version is emulated. constraintProblems lists, version by version, the cases in which the statements cannot hold: a reference or partner that is absent, a command placed relative to itself or in several ways at once, one claimed by several pairs, or a pair whose members are both placed.

Building

   npm i
   npm run build

Releasing

Releases go out through .github/workflows/publish.yml, never from a laptop: raise the version in package.json, commit, and push a tag v<version>, or run the workflow by hand from the Actions tab. The workflow builds, runs the tests and publishes with provenance through npm's trusted publishing, so no token is stored. It checks out the emulator beside this repository to build the T-100 entry point.