@stringsync/musicxml
v0.3.0
Published
a simple musicXML library
Downloads
11
Readme
musicxml
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).