Short scripts against an edition: reading it, listing its versions,
reading the text of one and what it changed, playing it, and changing the
edition. They follow on from each other, each taking up edition and
version from the ones before. The code is that of the library's tests,
which run it against a document in the repository, so it does what the
page says. The format of the documents is described at
https://pfefferniels.github.io/linked-rolls/.
An edition is published as a JSON-LD document.
importJsonLd reads it into plain data,
and brings a document written by an earlier release of the format up to
date on the way. validate holds a
document, not what the import returns, against the schema of the current
format, so a document of an earlier release fails until
migrate has brought it up to date; a current
document passes through migrate unchanged. Where a document fails,
validate.errors lists each fault with where it lies (instancePath, a
JSON pointer) and what is wrong there. A script reports them and stops
rather than reading on. file is the path of the document.
import { readFileSync } from 'node:fs'
import { importJsonLd, migrate } from 'linked-rolls'
import { validate } from 'linked-rolls/validate'
const json = JSON.parse(readFileSync(file, 'utf8'))
const document = migrate(json)
if (!validate(document)) {
const problems = (validate.errors ?? []).map(error => `${error.instancePath}: ${error.message}`)
throw new Error(`Not an edition:\n${problems.join('\n')}`)
}
const edition = importJsonLd(document)
A version is a state of the roll's text, defined by its edits against the
version it derives from; the versions and their derivations form the
stemma. A version does not store its siglum.
siglaOf reads the sigla off the stemma as it
stands: the letter names the reproducing system (R for the red Welte
T-100, G for the green, L for the Licensee), the number counts the
generations, and a number after a dot marks a branch. A version that no
copy shows at first hand, known only through the versions derived from
it, is in lowercase, as r1 is here. Since the sigla change whenever the
stemma does, a script or a citation names a version by its id, which the
document's base makes an IRI. derivationsOf
gives the versions a version derives from, each with the certainty the
derivation is held with.
import { derivationsOf, siglaOf, versionIn } from 'linked-rolls'
const sigla = siglaOf(edition)
for (const version of edition.versions) {
const parents = derivationsOf(version).map(({ parent, certainty }) => `${sigla.get(parent)} (${certainty})`)
console.log(sigla.get(version.id), version.system.name, new URL(version.id, edition.base).href, parents.join(', '))
}
// A siglum follows the stemma; the id stays.
const version = versionIn(edition, '19fd4209-81cc-4d03-b2c3-fc7518dbba14')!
A feature is something physical on one copy, above all a chain of holes.
A symbol is what the edition reads from it: a note with its pitch, an
expression command with its type and the half of the keyboard it acts on,
or a text such as a label. One symbol stands for the same perforation on
every copy that has it, and its carriers name those features by id.
snapshotOf gives the symbols a version
shows, what it and its ancestors insert less what they delete, in order of
place. placeOf gives where a symbol lies, as
the median of its carriers' places, in millimetres on the edition's axis.
That axis is the paper of the reference copy, measured from where its scan
begins. A document holds each copy's features at the copy's own places,
and the import has carried them onto the axis.
import { carriersOf, copyOfFeature, isCommand, placeOf, snapshotOf } from 'linked-rolls'
const text = snapshotOf(edition, version.id)
.filter(isCommand)
.map(command => ({
command: command.type === 'note' ? `note ${command.pitch}` : `${command.expressionType} (${command.scope})`,
place: placeOf(edition, command),
copies: carriersOf(edition, command).map(feature => copyOfFeature(edition, feature.id)?.id)
}))
A version states only how it differs from the version it derives from,
which predecessorOf gives. Each edit
inserts symbols, deletes by id symbols that version shows, or both, and
may name a motivation: one of the edition's motivations by id, or one the
vocabulary publishes by IRI. A collation
(connectVersions) writes each
difference it finds with the motivation its shape calls for: the generic
musical one, whose reason no editor has given yet, or the technical
one. Any
statement can carry a belief under @annotation, a JSON-LD-star
annotation that holds the statement with a certainty from true to
false for the reasons it lists. certaintyOf
reads the certainty; a statement without a belief is held true.
What kind of change an edit is follows from its motivation
(kindOfChange): the technical one
for a technical change, the generic musical one or one of the edition's own
for a musical change, and none for the text of a version that derives from
none.
proposeEdits groups what a version
changes into edits and reads the shape of each, an accent added, a command
put elsewhere, a note struck again, and
adoptEdits puts them in place of the
program's edits, keeping those somebody motivated.
import { certaintyOf, editsOf, kindOfChange, symbolsIn } from 'linked-rolls'
const motivationOf = (id?: string) => edition.motivations?.find(motivation => motivation.id === id)
const changes = editsOf(version).map(edit => ({
kind: kindOfChange(edit),
motivation: motivationOf(edit.motivation)?.note,
inserted: edit.insert ?? [],
deleted: symbolsIn(edition, edit.delete ?? []),
certainty: certaintyOf(edit),
reasons: edit['@annotation']?.belief.reasons ?? []
}))
emulate performs a version on a reproducing
system: the commands are placed on the paper, and the system's model of
the instrument turns the paper into time, dynamics and pedalling. The
Welte-Mignon systems have entry points of their own,
linked-rolls/welte-t100, linked-rolls/welte-t98 and
linked-rolls/welte-licensee, and need the optional peer dependency
welte-mignon-emulator; use
the one the version is coded for (version.system).
midiOf gives the performance as a MIDI file in
the form midifile-ts reads and writes, each note labelled with the id of
the symbol it performs; write turns it into the bytes of a standard
MIDI file. out is the path of the MIDI file.
import { writeFileSync } from 'node:fs'
import { write } from 'midifile-ts'
import { emulate, midiOf } from 'linked-rolls'
import { welteT100System } from 'linked-rolls/welte-t100'
const { events, source } = emulate(welteT100System, version, edition)
const midi = midiOf(events, welteT100System.name, welteT100System.defaultOptions, source)
writeFileSync(out, write(midi.tracks, midi.header.ticksPerBeat))
An edition is frozen once anything has been asked of it, since what the
library works out from a state is kept for as long as the state lasts. It
is changed through operations, never in place. An operation is a function
on an immer draft of the edition:
produce applies one, or several in turn, and returns the new state,
leaving the old one as it was. Roll Desk makes each operation one step to
undo. The operations on beliefs take the path to the statement, which
pathIn finds from its id.
asJsonLd turns the edition back into a
document, each copy's features at the copy's own places again. The
published edition of WM 225 is written with four spaces of indentation and
a final newline, so that a diff between two states shows only what
changed.
import { writeFileSync } from 'node:fs'
import { produce } from 'immer'
import { addReason, asJsonLd, createBelief, editsOf, pathIn, setCertainty } from 'linked-rolls'
import { validate } from 'linked-rolls/validate'
const edit = editsOf(version).find(({ motivation }) => motivation === 'less-accent-on-f')!
const path = pathIn(edition, edit.id)!
const changed = produce(edition, draft => {
createBelief(path)(draft)
setCertainty(path, 'likely')(draft)
addReason(path, { type: 'simpleArgumentation', note: 'The change is clear on one copy only.' })(draft)
})
const output = asJsonLd(changed)
if (!validate(output)) throw new Error('The changed edition does not hold to the schema')
writeFileSync(file, JSON.stringify(output, null, 4) + '\n')