Skip to content

stringsync/vexml

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2,342 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vexml

test workflow

Demo

vexml is an open source library that renders MusicXML using vexflow.

Usage

Installation

npm install @stringsync/vexml

Note

I recommend you to lock into a specific version to avoid breakages due to vexml API changes.

⚠️ IMPORTANT: Loading Fonts

vexml uses vexflow 5^, which requires you to load the fonts you need to use. See the vexflow repo for more information.

Rendering

Rendering requires you to provide a valid MusicXML string and an HTMLDivElement.

import * as vexml from '@stringsync/vexml';

const musicXML = 'some valid musicXML string';
const div = document.getElementById('my-id');
const score = vexml.renderMusicXML(musicXML, div);

You can also render MXL given a Blob input.

import * as vexml from '@stringsync/vexml';

const mxl = new Blob(['some', 'valid', 'mxl', 'bytes']);
const div = document.getElementById('my-id');
const scorePromise = vexml.renderMXL(musicXML, div);
// From here, you need to await or call then() on the scorePromise to extract the score.

Advanced Usage

Config

To see an exhaustive list of configuration options, see config.ts. You can experiment with all configs on dev site or vexml.dev.

Events

The event listening API is similar to EventTarget.addEventListener, except you need to save a reference to the returned handle to unsubscribe.

const score = vexml.renderMusicXML(musicXML, div);

const handle = score.addEventListener('click', (e) => {
  console.log(e);
});

// ...

score.removeEventListener(handle);

Events work the same for both canvas and svg backends.

Cursors

Cursors mark a position in a rendered vexml score. You can step through a piece entry-by-entry or seek a specific timestamp.

  • Add a cursor model for the part you're interested in.
  • Render a cursor component to the score's overlay.
  • Listen update the component to react to model changes.

Tip

Rendering a cursor component is optional.

import * as vexml from '@stringsync/vexml';

// ...

const score = vexml.renderMusicXML(musicXML, div);

// Add
const cursorModel = score.addCursor();

// Render
const cursorComponent = vexml.SimpleCursor.render(score.getOverlayElement());

// Listen
cursorModel.addEventListener(
  'change',
  (e) => {
    cursorComponent.update(e.rect);
    // The model infers its visibility via the rect. It assumes you've updated appropriately.
    if (!cursorModel.isFullyVisible()) {
      cursorModel.scrollIntoView(scrollBehavior);
    }
  },
  { emitBootstrapEvent: true }
);

See Vexml.tsx for an example in React.

Custom Rendering

renderMusicXML is the standard way to orchestrate vexml objects. If you need more grannular control, you need to do the following:

  • Declare a vexml configuration.
  • Parse the MusicXML into a vexml data document.
  • Format the vexml data document.
  • Render the formatted vexml data document.
import * as vexml from '@stringsync/vexml';

// Declare
const config = { ...vexml.DEFAULT_CONFIG, WIDTH: 600 };
const logger = new vexml.ConsoleLogger();

// Parse
const parser = new vexml.MusicXMLParser({ config });
const document = parser.parse(musicXML);

// Format
const defaultFormatter = new vexml.DefaultFormatter({ config });
const monitoredFormatter = new vexml.MonitoredFormatter(defaultFormatter, logger, { config });
const formattedDocument = monitoredFormatter.format(document);

// Render
const renderer = new vexml.Renderer({ config, formatter: monitoredFormatter, logger });
const score = renderer.render(div, formattedDocument);

Important

I highly recommend you pass the same config object to all vexml objects. Otherwise, you may get unexpected results.

See render.ts and Vexml.tsx for more examples.

Gap Measures

Gap measures are non-musical fragments that optionally have a label. This is useful when syncing a vexml cursor with media that has non-musical pauses in it (e.g. a video of a teacher explaining a musical concept).

gap measure example

You should create these right after you parse a document, specifically before format you it. Otherwise, the gap may invalidate the format's output.

// ...

const parser = new vexml.MusicXMLParser({ config });
const document = parser.parse(musicXML);

// Insert the gap measure **before** formatting.
document.insertGapMeasureBefore({
  absoluteMeasureIndex: 0,
  durationMs: 5000,
  minWidth: 500,
  label: 'What are pitches?',
  style: {
    fontSize: '16px',
  },
});

const formatter = new vexml.DefaultFormatter({ config });

// render, etc.

Development

Prerequisites

You need Node.js >= 22.6 to run the vex CLI (it relies on --experimental-strip-types to execute TypeScript directly, which was added in Node.js 22.6.0).

You need docker to run the integration tests.

Installing

Before you run any commands, install the dependencies.

npm install

The vex CLI

All dev tasks run through the vex CLI defined under cli/. After npm install, you have two ways to invoke it:

  • npx vex <command> — works out of the box, no extra setup.
  • vex <command> — shorter, but requires a one-time install. Run npm link from the repo root to add vex to your PATH; run npm unlink from the repo root to remove it.
npx vex --help
# or, after `npm link`:
vex --help

The rest of this README uses npx vex for the examples; substitute vex if you've installed it globally.

The CLI requires Node.js >= 22.6 (it uses --experimental-strip-types to run TypeScript directly).

Running the Dev Server

In order to run a dev server that hot reloads vexml changes, run:

npx vex dev

You should be able to "save" MusicXML documents in localstorage using the dev app, which will cause the documents to survive refreshing the page.

Running Tests

In order to run tests on x86 architecture, run:

npx vex test

If you're running a machine using ARM architecture (such as an M series mac), try setting the default platform before running the command (or set it in your shell profile):

export DOCKER_DEFAULT_PLATFORM=linux/amd64

Extra arguments are forwarded to jest, so you can use all the jest CLI options. For example, to run in watch mode:

npx vex test --watchAll

To skip Docker and run jest directly against your local environment, use --local (or -l). This is faster, but image snapshots will likely diff against the canonical Docker-rendered ones:

npx vex test --local

If you suspect issues with the tmp snapshots, run the following command to retake the snapshots from origin/master:

npx vex resnap

Snapshots

This library uses americanexpress/jest-image-snapshot for image-based snapshot tests.

Diffs

You can see diff images in the __diff_output__ directory (nested under __image_snapshots__). Images here are ignored by git, but allow you to see what changed. The order of images are: snapshot, diff, received.

Updating Snapshots

Rendering varies by platform, so it is important you run tests using npx vex test, since that runs tests in a consistent environment (via Docker). This is also the environment that CI will use.

When you want to update all snapshots, rerun the test command with --updateSnapshot.

npx vex test --updateSnapshot

If you want to only update a single snapshot from specific test, you can use --testNamePattern.

Removing tests

When removing a test, it is important to remove the corresponding snapshot. There is currently no automatic mechanism to do this. Alternatively, you can run the vexml:latest Docker image with JEST_IMAGE_SNAPSHOT_TRACK_OBSOLETE=1, but make sure you're running the entire test suite. See the docs for more information.

Publishing

You can publish a vexml version by running the release script:

npx vex release [patch|minor|major]

It should create the git tags needed to create a release on GitHub.

Design

Rendering Hiearchy

The rendering hiearchy is the most important data structure in vexml. The tree-like structure is what allows vexml to query data efficiently in the data document instead of manually passing data directly from a node to its ancestors.

Note

You can visualize most of these structures on vexml.dev by enabling the DEBUG_DRAW_* options.

Score

Score is the root of the hiearchy. It contains many systems and non-musical engravings such as the title.

system

System

System represents a collection of formatted measures across all parts.

score

Measure

Measure represents a collection formatted fragments across all parts.

measure

Fragment

Fragment represents a collection of parts formatted together. It is a music section with a distinct fragment signature (see data/types.ts for what makes a fragment signature). It is necessary because there are some elements that vexflow can only render one of per stave (e.g. start clef). Fragment also contains some non-musical elements, such as part labels.

fragment

Part

Part is a fragment-scoped music section that usually corresponds to an instrument. It contains many staves.

part

Stave

Stave is a container for voices.

stave

Shown here are stave intrinsic rects.

Voice

Voice represents a collection of entries. There can be multiple voices per stave.

voice

Voice Entry

Voice entry is anything rendered within a voice. Some common examples are notes and rests.

voice entry

About

MusicXML to Vexflow

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages