satie
Version:
A sheet music renderer for the web
689 lines (671 loc) • 21.5 kB
text/typescript
/**
* This file is part of Satie music engraver <https://github.com/jnetterf/satie>.
* Copyright (C) Joshua Netterfield <joshua.ca> 2015 - present.
*
* Satie is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Satie is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Satie. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file part of Satie test suite
*/
import {DirectionMode, EnclosureShape, NormalItalic, NormalBold, LeftCenterRight,
TopMiddleBottomBaseline, parseScore} from "musicxml-interfaces";
import {forEach, some} from "lodash";
import {expect} from "chai";
import Factory from "../engine_factory";
import {_extractMXMLHeader, _extractMXMLPartsAndMeasures} from "../engine_import";
import AttributesExports from "../implAttributes_attributesModel";
import Barline from "../implBarline_barlineModel";
import Chord from "../implChord_chordModel";
import Direction from "../implDirection_directionModel";
import Print from "../implPrint_printModel";
import Sound from "../implSound_soundModel";
import Spacer from "../implSpacer_spacerModel";
import ScoreHeader from "../engine_scoreHeader";
/*---- samples ----------------------------------------------------------------------------------*/
let helloWorldXML = `
<score-partwise version="3.0">
<movement-title>Song Title</movement-title>
<identification>
<creator type="composer">Song Composer</creator>
<creator type="lyricist">Song Lyricist</creator>
<creator type="arranger">Song Arranger</creator>
<rights>Song Copyright</rights>
<encoding>
<software>Song Software 1</software>
<software>Song Software 2</software>
<encoding-date>2015-03-10</encoding-date>
<supports attribute="new-page" element="print" type="yes" value="yes"/>
<supports element="accidental" type="yes"/>
</encoding>
</identification>
<credit page="1">
<credit-type>title</credit-type>
<credit-words default-x="597" default-y="1440" font-size="24" justify="center" valign="top">
Song Title
</credit-words>
</credit>
<credit page="1">
<credit-type>composer</credit-type>
<credit-words default-x="1124" default-y="1362" font-size="12" justify="right" valign="top">
Song Composer
</credit-words>
</credit>
<credit page="1">
<credit-type>rights</credit-type>
<credit-words default-x="597" default-y="70" font-size="10" justify="center" valign="bottom">
Song Copyright
</credit-words>
</credit>
<credit page="1">
<credit-words default-x="70" default-y="1453" font-size="12" valign="top">
Score
</credit-words>
</credit>
<credit page="2">
<credit-type>rights</credit-type>
<credit-words default-x="597" default-y="70" font-size="10" justify="center" valign="bottom">
Song Copyright
</credit-words>
</credit>
<part-list>
<score-part id="P1">
<part-name print-object="no">MusicXML Part</part-name>
<score-instrument id="P1-I1">
<instrument-name>SmartMusic SoftSynth 1</instrument-name>
</score-instrument>
<midi-instrument id="P1-I1">
<midi-channel>1</midi-channel>
<midi-bank>15489</midi-bank>
<midi-program>1</midi-program>
<volume>80</volume>
<pan>0</pan>
</midi-instrument>
</score-part>
</part-list>
<part id="P1">
<measure number="1" width="983">
<print>
<system-layout>
<system-margins>
<left-margin>70</left-margin>
<right-margin>0</right-margin>
</system-margins>
<top-system-distance>211</top-system-distance>
</system-layout>
<measure-numbering>system</measure-numbering>
</print>
<attributes>
<divisions>2</divisions>
<key>
<fifths>0</fifths>
<mode>major</mode>
</key>
<time>
<beats>4</beats>
<beat-type>4</beat-type>
</time>
<clef>
<sign>G</sign>
<line>2</line>
</clef>
</attributes>
<sound tempo="120"/>
<note>
<rest measure="yes"/>
<duration>8</duration>
<voice>1</voice>
</note>
<barline location="right">
<bar-style>light-heavy</bar-style>
</barline>
</measure>
</part>
</score-partwise>
`;
let lily43eXML = `
<score-partwise>
<identification>
<miscellaneous>
<miscellaneous-field name="description">A piano staff with dynamics and
clef changes, where each element (ffff, wedge and clef changes)
applies only to one voice or one staff, respectively.</miscellaneous-field>
</miscellaneous>
</identification>
<part-list>
<score-part id="P1">
<part-name>MusicXML Part</part-name>
</score-part>
</part-list>
<!--=========================================================-->
<part id="P1">
<measure number="1">
<attributes>
<divisions>8</divisions>
<key>
<fifths>0</fifths>
<mode>major</mode>
</key>
<time symbol="common">
<beats>4</beats>
<beat-type>4</beat-type>
</time>
<staves>2</staves>
<clef number="1">
<sign>G</sign>
<line>2</line>
</clef>
<clef number="2">
<sign>F</sign>
<line>4</line>
</clef>
</attributes>
<direction placement="below">
<direction-type>
<dynamics>
<ffff/>
</dynamics>
</direction-type>
<staff>1</staff>
</direction>
<note>
<pitch>
<step>C</step>
<octave>5</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<note>
<pitch>
<step>B</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<note>
<pitch>
<step>A</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<direction placement="below">
<direction-type>
<dynamics>
<p/>
</dynamics>
</direction-type>
<offset>1</offset>
<staff>1</staff>
</direction>
<note>
<pitch>
<step>G</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<backup>
<duration>32</duration>
</backup>
<direction placement="below">
<direction-type>
<wedge spread="0" type="crescendo"/>
</direction-type>
<staff>2</staff>
</direction>
<note>
<pitch>
<step>A</step>
<octave>2</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>B</step>
<octave>2</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<direction>
<direction-type>
<wedge spread="15" type="stop"/>
</direction-type>
<staff>2</staff>
</direction>
<note>
<pitch>
<step>C</step>
<octave>3</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>D</step>
<octave>3</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
</measure>
<!--=======================================================-->
<measure number="2">
<attributes>
<key>
<fifths>2</fifths>
<mode>major</mode>
</key>
<clef number="2">
<sign>G</sign>
<line>2</line>
</clef>
</attributes>
<note>
<pitch>
<step>A</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<note>
<pitch>
<step>B</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<note>
<pitch>
<step>C</step>
<alter>1</alter>
<octave>5</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<note>
<pitch>
<step>D</step>
<octave>5</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<backup>
<duration>32</duration>
</backup>
<note>
<pitch>
<step>F</step>
<alter>1</alter>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>G</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>A</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>B</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
</measure>
<!--=======================================================-->
<measure number="3">
<attributes>
<clef number="1">
<sign>C</sign>
<line>2</line>
</clef>
</attributes>
<note>
<pitch>
<step>D</step>
<octave>5</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<note>
<pitch>
<step>C</step>
<alter>1</alter>
<octave>5</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<note>
<pitch>
<step>B</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<note>
<pitch>
<step>A</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
<backup>
<duration>32</duration>
</backup>
<note>
<pitch>
<step>A</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>B</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>C</step>
<alter>1</alter>
<octave>5</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>D</step>
<octave>5</octave>
</pitch>
<duration>8</duration>
<voice>2</voice>
<type>quarter</type>
<staff>2</staff>
</note>
</measure>
<!--=======================================================-->
<measure number="4">
<note>
<rest/>
<duration>32</duration>
<voice>1</voice>
<staff>1</staff>
</note>
<backup>
<duration>32</duration>
</backup>
<note>
<rest/>
<duration>32</duration>
<voice>2</voice>
<staff>2</staff>
</note>
<barline location="right">
<bar-style>light-heavy</bar-style>
</barline>
</measure>
</part>
<!--=========================================================-->
</score-partwise>`;
describe("[musicxml/import.ts]", function() {
describe("_extractMXMLHeader", function() {
it("can parse all header properties", function() {
let mxmljson = parseScore(helloWorldXML);
let header = _extractMXMLHeader(mxmljson);
expect(header).to.be.an.instanceof(ScoreHeader);
expect(header.credits.length).to.eq(5);
expect(header.credits[4].page).to.eq(2);
expect(header.credits[0].creditTypes).to.deep.equal(["title"]);
expect(header.credits[1].creditWords).to.deep.equal([{
color: "#000000",
defaultX: 1124,
defaultY: 1362,
dir: DirectionMode.Ltr,
enclosure: EnclosureShape.None,
fontFamily: "",
fontSize: "12",
fontStyle: NormalItalic.Normal,
fontWeight: NormalBold.Normal,
halign: LeftCenterRight.Right, // Agrees with justify
justify: LeftCenterRight.Right,
letterSpacing: "normal",
lineHeight: "normal",
lineThrough: 0,
overline: 0,
relativeX: null,
relativeY: null,
rotation: 0,
underline: 0,
valign: TopMiddleBottomBaseline.Top,
words: "Song Composer"
}]);
// Check that halign still follows justify
expect(header.credits[0].creditWords[0].halign).to.eq(LeftCenterRight.Center);
expect(header.identification.creators.length).to.eq(3);
expect(header.identification.creators[0].type).to.eq("composer");
expect(header.identification.creators[0].creator).to.eq("Song Composer");
expect(header.identification.creators[1].type).to.eq("lyricist");
expect(header.identification.creators[1].creator).to.eq("Song Lyricist");
expect(header.identification.creators[2].type).to.eq("arranger");
expect(header.identification.creators[2].creator).to.eq("Song Arranger");
expect(header.identification.encoding).to.deep.equal({
encoders: [],
encodingDate: {
day: 10,
month: 3,
year: 2015
},
encodingDescriptions: [],
softwares: [ // TODO: musicxml-interfaces: shouldn't be plural
"Song Software 1",
"Song Software 2"
],
supports: {
accidental: {
attribute: "",
element: "accidental",
type: true,
value: ""
},
print_newPage: {
attribute: "new-page",
element: "print",
type: true,
value: "yes"
}
}
});
expect(header.movementTitle).to.eq("Song Title");
expect(header.partList).to.deep.equal([
{
_class: "ScorePart",
groups: [],
id: "P1",
identification: null,
midiDevices: [],
midiInstruments: [{
elevation: null,
id: "P1-I1",
midiBank: 15489,
midiChannel: 1,
midiName: "",
midiProgram: 1,
midiUnpitched: null,
pan: 0,
volume: 80
}],
partAbbreviation: null,
partAbbreviationDisplay: null,
partName: {
color: "#000000",
defaultX: null,
defaultY: null,
fontFamily: "",
fontSize: "",
fontStyle: NormalItalic.Normal,
fontWeight: NormalBold.Normal,
justify: LeftCenterRight.Left,
partName: "MusicXML Part",
printObject: false,
relativeX: null,
relativeY: null
},
partNameDisplay: null,
scoreInstruments: [{
ensemble: "",
id: "P1-I1",
instrumentAbbreviation: "",
instrumentName: "SmartMusic SoftSynth 1",
instrumentSound: "",
solo: null,
virtualInstrument: null
}]
}
]);
expect(!("parts" in header), "Check _extractHeader");
// Extensions
expect(header.composer).to.eq("Song Composer");
expect(header.title).to.eq("Song Title");
});
});
describe("_extractMXMLPartsAndMeasures", function() {
it("parses a basic single-part song", function() {
let factory = new Factory([AttributesExports, Chord, Print, Sound, Barline]);
// does not need spacer
let mxmljson = parseScore(helloWorldXML);
let partsAndMeasures = _extractMXMLPartsAndMeasures(mxmljson, factory);
expect(partsAndMeasures.measures.length).to.eq(1);
expect(partsAndMeasures.measures[0].parts["P1"].staves[1].length).to.eq(4);
expect(partsAndMeasures.measures[0].parts["P1"].staves[1][0].divCount).to.eq(0);
expect(partsAndMeasures.measures[0].parts["P1"].staves[1][1].divCount).to.eq(0);
expect(partsAndMeasures.measures[0].parts["P1"].staves[1][2].divCount).to.eq(8);
expect(partsAndMeasures.measures[0].parts["P1"].staves[1][3].divCount).to.eq(0);
expect(partsAndMeasures.measures[0].parts["P1"].voices[1].length).to.eq(1);
});
it("parses multi-voice, multi-staff songs with backup", function() {
let factory = new Factory([AttributesExports, Direction, Chord, Print, Sound, Barline, Spacer]);
let mxmljson = parseScore(lily43eXML);
let partsAndMeasures = _extractMXMLPartsAndMeasures(mxmljson, factory);
expect(partsAndMeasures.measures.length).to.eq(4);
expect(partsAndMeasures.parts).to.eql(["P1"]);
let voices = partsAndMeasures.measures[0].parts["P1"].voices;
let staves = partsAndMeasures.measures[0].parts["P1"].staves;
expect(!voices[0]).to.eq(true, "voices are 1-indexed");
expect(!staves[0]).to.eq(true, "staves are 1-indexed");
expect(voices.length).to.eq(3);
expect(staves.length).to.eq(3);
expect(voices[2].owner).to.eq(2);
expect(voices[2].divisions).to.eq(8);
expect(voices[1].divisions).to.eq(8);
expect(staves[2].divisions).to.eq(8);
expect(staves[1].divisions).to.eq(8);
expect(staves[1].length).to.eq(3);
expect(staves[2].length).to.eq(3);
forEach(staves[1], model => {
expect(!some(staves[2], m2 => model === m2));
});
forEach(voices[1], model => {
expect(!some(voices[2], m2 => model === m2));
});
});
});
describe("toScore", function() {
// todo
});
});