@stringsync/musicxml
TypeScript icon, indicating that this package has built-in type declarations

0.3.0 • Public • Published

musicxml

test workflow

See the docs.

musicxml is a JavaScript library that makes it easy to parse and edit MusicXML documents.

One of the common problems working with MusicXML is that different softwares may export invalid MusicXML documents due to the complex nature of the MusicXML specification. This library guarantees that parsed and serialized MusicXML documents are valid by conforming the document to the specification.

⚠️ Warning

API

This API is unstable - use at your own risk. I highly recommend that you lock into a specific version of this library.

Lossy Parsing

When parsing a MusicXML document, musicxml will ignore comments and treat CDATA as regular text data. When serializing back to xml, comments are completely ommitted and CDATA is rendered as text nodes, since xml-js will escape special characters.

In order to guarantee that parsed and serialized documents are valid, this library will replace invalid element or text nodes with a default value. See src/lib/operations/zero.ts for how the default values are determined.

🔨 Usage

Installation

I highly recommend that you lock into a specific version of this library.

yarn add @stringsync/musicxml@0.2.1

or

npm install @stringsync/musicxml@0.2.1

Exports

import { asserts, elements, MusicXML } from '@stringsync/musicxml';

Parse and serialize a MusicXML document

const xml = `<?xml version="1.0" encoding="UTF-8"?>
<score-partwise version="4.0">
  <part-list>
    <score-part id="P1">
      <part-name></part-name>
    </score-part>
  </part-list>
  <part id="P1">
    <measure number="1"/>
  </part>
</score-partwise>`;

// parse
const musicXml = MusicXML.parse(xml);

// serialize
console.log(musicXml.serialize() === xml); // true

Create and update elements

const measure = new elements.MeasurePartwise({ attributes: { number: '1', implicit: 'no' } });
measure.getNumber(); // '1'
measure.setNumber('4');
measure.getNumber(); // '4'
measure.setValues([...measure.getValues(), new elements.Note()]);

Chain setters methods

const note = new elements.Note();
note
  .setColor('#800080') // chain attributes
  .setStaff(new elements.Staff()) // chain contents
  .getStaff()!
  .setValue(4);

Create a MusicXML object

<score-partwise version="4.0"> root

const musicXml = MusicXML.createPartwise();
const root = musicXml.getRoot();
console.log(MusicXML.isScorePartwise(root)); // true

<score-timewise> root

const musicXml = MusicXML.createTimewise();
const root = musicXml.getRoot();
console.log(MusicXML.isScoreTimewise(root)); // true

Narrow types

Some types can be complex unions. For example, take the elements.Note class, which corresponds to the <note> element (see the content). The first content can be one of several different choices. This library expresses the choices in terms of a union.

// truncated elements.Note class

export type TiedNote = [Chord | null, Pitch | Unpitched | Rest, Duration, [] | [Tie] | [Tie, Tie]];

export type CuedNote = [Cue, Chord | null, Pitch | Unpitched | Rest, Duration];

export type TiedGraceNote = [Grace, Chord | null, Pitch | Unpitched | Rest, [] | [Tie] | [Tie, Tie]];

export type CuedGraceNote = [Grace, Cue, Chord | null, Pitch | Unpitched | Rest, Duration];

class Note {
  getVariation(): TiedNote | CuedNote | TiedGraceNote | CuedGraceNote {
    return this.contents[0];
  }
}

It is very cumbersome to manually validate which choice is being used. Also, the lack of overlap of some choices can make it difficult to use with TypeScript. The asserts export will have type predicates corresponding to those choices.

For example, to work with an elements.Note value:

const note = new elements.Note();
const noteVariation = note.getVariation();

if (asserts.isTiedNote(noteVariation)) {
  // noteVariation: TiedNote
} else if (asserts.isCuedNote(noteVariation)) {
  // noteVariation: CuedNote
} else if (asserts.isTiedGraceNote(noteVariation)) {
  // noteVariation: TiedGraceNote
} else if (asserts.isCuedGraceNote(noteVariation)) {
  // noteVariation: CuedGraceNote
} else {
  // noteVariation: never
}

💻 Development

Prerequisites

musicxml uses Docker and Docker Compose to create the test environment. The tests will not pass if you try to run them locally.

Testing

musicxml uses xsdvalidate to validate XML against an xsd schema. This library is exposed as an HTTP service in the xmlvalidator directory. The schema was adapted directly from w3.

To run the tests, run the following in the project directory:

yarn test

musicxml uses the jest testing framework. You can pass any of the jest CLI options to the test command. For example, to run the tests in watch mode (recommended), run:

yarn test --watchAll

A complete list of options are in the jest docs.

❓ FAQs

Why didn't you derive the elements directly from musicxml.xsd?

This is something I've been frequently asking myself.

I've tried multiple times to get this to work, but xsd has been extremely challenging to work with. I tried making a pared down xsd parser that would work with the parts used in musicxml.xsd, but it was still incredibly challenging.

The problem came down to parsing the contents of an xsd element. The main two approaches I tried were: (1) making a sax-like state machine and (2) writing imperative routines to parse the xsd. Take this <xs:schema> definition for example:

<schema
  attributeFormDefault = (qualified | unqualified): unqualified
  blockDefault = (#all | List of (extension | restriction | substitution) : ''
  elementFormDefault = (qualified | unqualified): unqualified
  finalDefault = (#all | List of (extension | restriction | list |
union): ''
  id = ID
  targetNamespace = anyURI
  version = token
  xml:lang = language
  {any attributes with non-schema Namespace}...>
Content: ((include | import | redefine | annotation)*, (((simpleType |
complexType | group | attributeGroup) | element | attribute | notation),
annotation*)*)
</schema>

Pay attention to how the annotation element could appear multiple times in the beginning and end of the element's content. This means I cannot simply index the contents into properties. This was a somewhat common occurence in the elements used in musicxml.xsd. I had to maintain the groupings of elements.

Thanks to xml-js, I was able to get some intermediate structure that looked like this:

{
  "type": "element",
  "name": "xs:schema",
  "attributes": { /* ... */ },
  "contents": [
    { "type": "element", "name": "annotation", "attributes": { /* ... */ }, contents: [ /* ... */ ] }
    { "type": "element", "name": "import", "attributes": { /* ... */ }, contents: [ /* ... */ ] }
    { "type": "element", "name": "import", "attributes": { /* ... */ }, contents: [ /* ... */ ] }
    { "type": "element", "name": "import", "simpleType": { /* ... */ }, contents: [ /* ... */ ] }
    { "type": "element", "name": "import", "simpleType": { /* ... */ }, contents: [ /* ... */ ] }
    // etc.
  ]
}

In the state machine approach, it was difficult to simulate stack frames in order to "jump back" to a particular place. I tried using object paths, but they were ultimately messy and troublesome.

In the imperative approach, there were so many elements and nested groupings and it would take a while to implement correctly. I would want the imperative approach to be reasonably tested, making this too expensive for me to pursue, which is why I ultimately moved away from that approach.

That was just issues with parsing the xsd. I didn't even get to the point where I could conform an xml document to an xsd file.

For whoever wants to revisit this and make the generated client solely based off of musicxml.xsd, I highly recommend that you leverage some library that does the heavy lifting xsd parsing for you. At the time of writing this, I did not find any actively maintained candidates. Conformance of an xml document against an xsd schema is a separate problem.

All in all, rolling my own descriptor/schema library within this package (see src/lib/schema) allowed me to use a structure that was more compatible with TypeScript. I considered transforming musicxml.xsd into these descriptors, but I wrote labeling functionality that made it easy to reference logical groups. For example, the element has multiple choices for what its main content can be. musicxml.xsd does not name these choices. In my descriptor library, I name them making them easier to work with (see src/lib/elements/Note.ts).

Readme

Keywords

Package Sidebar

Install

npm i @stringsync/musicxml

Weekly Downloads

2

Version

0.3.0

License

MIT

Unpacked Size

5.14 MB

Total Files

70

Last publish

Collaborators

  • jaredjj3