UNPKG

satie

Version:

A sheet music renderer for the web

689 lines (671 loc) 21.5 kB
/** * 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 = `<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> <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 = `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 1.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> <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 }); });