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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@stringsync/musicxml

v0.3.0

Published

a simple musicXML library

Downloads

11

Readme

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/[email protected]

or

npm install @stringsync/[email protected]

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