linked-rolls
    Preparing search index...

    Recipes

    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')