Source: music21/musicxml/m21ToXml.js

/**
 * @namespace music21.musicxml.m21ToXml
 */
import * as clef from '../clef.js';
import * as common from '../common.js';
import {  // eslint-disable-line import/no-cycle
    Stream, Measure, Part, Score,
} from '../stream.js';

import { Music21Exception } from '../exceptions21.js';

class MusicXMLExportException extends Music21Exception {

}

function typeToMusicXMLType(value) {
    if (value === 'longa') {
        return 'long';
    } else if (value === '2048th') {
        throw new MusicXMLExportException('Cannot convert "2048th" duration to MusicXML (too short).');
    } else {
        return value;
    }
}

function normalizeColor(color) {
    const colors = {
        'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff',
        'aquamarine': '#7fffd4', 'azure': '#f0ffff',
        'beige': '#f5f5dc', 'bisque': '#ffe4c4', 'black': '#000000',
        'blanchedalmond': '#ffebcd', 'blue': '#0000ff', 'blueviolet': '#8a2be2',
        'brown': '#a52a2a', 'burlywood': '#deb887',
        'cadetblue': '#5f9ea0', 'chartreuse': '#7fff00', 'chocolate': '#d2691e',
        'coral': '#ff7f50', 'cornflowerblue': '#6495ed', 'cornsilk': '#fff8dc',
        'crimson': '#dc143c', 'cyan': '#00ffff',
        'darkblue': '#00008b', 'darkcyan': '#008b8b', 'darkgoldenrod': '#b8860b',
        'darkgray': '#a9a9a9', 'darkgreen': '#006400', 'darkkhaki': '#bdb76b',
        'darkmagenta': '#8b008b', 'darkolivegreen': '#556b2f',
        'darkorange': '#ff8c00', 'darkorchid': '#9932cc', 'darkred': '#8b0000',
        'darksalmon': '#e9967a', 'darkseagreen': '#8fbc8f', 'darkslateblue': '#483d8b',
        'darkslategray': '#2f4f4f', 'darkturquoise': '#00ced1',
        'darkviolet': '#9400d3', 'deeppink': '#ff1493', 'deepskyblue': '#00bfff',
        'dimgray': '#696969', 'dodgerblue': '#1e90ff',
        'firebrick': '#b22222', 'floralwhite': '#fffaf0', 'forestgreen': '#228b22',
        'fuchsia': '#ff00ff',
        'gainsboro': '#dcdcdc', 'ghostwhite': '#f8f8ff', 'gold': '#ffd700',
        'goldenrod': '#daa520', 'gray': '#808080', 'green': '#008000',
        'greenyellow': '#adff2f',
        'honeydew': '#f0fff0', 'hotpink': '#ff69b4',
        'indianred ': '#cd5c5c', 'indigo': '#4b0082', 'ivory': '#fffff0',
        'khaki': '#f0e68c',
        'lavender': '#e6e6fa', 'lavenderblush': '#fff0f5', 'lawngreen': '#7cfc00',
        'lemonchiffon': '#fffacd', 'lightblue': '#add8e6', 'lightcoral': '#f08080',
        'lightcyan': '#e0ffff', 'lightgoldenrodyellow': '#fafad2',
        'lightgrey': '#d3d3d3', 'lightgreen': '#90ee90', 'lightpink': '#ffb6c1',
        'lightsalmon': '#ffa07a', 'lightseagreen': '#20b2aa', 'lightskyblue': '#87cefa',
        'lightslategray': '#778899', 'lightsteelblue': '#b0c4de',
        'lightyellow': '#ffffe0', 'lime': '#00ff00', 'limegreen': '#32cd32',
        'linen': '#faf0e6',
        'magenta': '#ff00ff', 'maroon': '#800000', 'mediumaquamarine': '#66cdaa',
        'mediumblue': '#0000cd', 'mediumorchid': '#ba55d3', 'mediumpurple': '#9370d8',
        'mediumseagreen': '#3cb371', 'mediumslateblue': '#7b68ee',
        'mediumspringgreen': '#00fa9a', 'mediumturquoise': '#48d1cc',
        'mediumvioletred': '#c71585', 'midnightblue': '#191970', 'mintcream': '#f5fffa',
        'mistyrose': '#ffe4e1', 'moccasin': '#ffe4b5',
        'navajowhite': '#ffdead', 'navy': '#000080',
        'oldlace': '#fdf5e6', 'olive': '#808000', 'olivedrab': '#6b8e23',
        'orange': '#ffa500', 'orangered': '#ff4500', 'orchid': '#da70d6',
        'palegoldenrod': '#eee8aa', 'palegreen': '#98fb98', 'paleturquoise': '#afeeee',
        'palevioletred': '#d87093', 'papayawhip': '#ffefd5', 'peachpuff': '#ffdab9',
        'peru': '#cd853f', 'pink': '#ffc0cb', 'plum': '#dda0dd', 'powderblue': '#b0e0e6',
        'purple': '#800080',
        'rebeccapurple': '#663399', 'red': '#ff0000', 'rosybrown': '#bc8f8f',
        'royalblue': '#4169e1',
        'saddlebrown': '#8b4513', 'salmon': '#fa8072', 'sandybrown': '#f4a460',
        'seagreen': '#2e8b57', 'seashell': '#fff5ee', 'sienna': '#a0522d',
        'silver': '#c0c0c0', 'skyblue': '#87ceeb', 'slateblue': '#6a5acd',
        'slategray': '#708090', 'snow': '#fffafa', 'springgreen': '#00ff7f',
        'steelblue': '#4682b4',
        'tan': '#d2b48c', 'teal': '#008080', 'thistle': '#d8bfd8', 'tomato': '#ff6347',
        'turquoise': '#40e0d0',
        'violet': '#ee82ee',
        'wheat': '#f5deb3', 'white': '#ffffff', 'whitesmoke': '#f5f5f5',
        'yellow': '#ffff00', 'yellowgreen': '#9acd32',
    };
    if (color === undefined || color === '') {
        return color;
    } else if (!color.startsWith('#')) {
        return colors[color].toUpperCase();
    } else {
        return color.toUpperCase();
    }
}


const _classMapping = [
    'Score', 'Part', 'Measure', 'Voice', // 'Stream',
    'GeneralNote', // 'Pitch', 'Duration', 'Dynamic', 'DiatonicScale', 'Scale',
    // 'Music21Object',
];

export class GeneralObjectExporter {
    constructor(obj) {
        this.generalObj = obj;
    }

    parse(obj) {
        if (obj === undefined) {
            obj = this.generalObj;
        }
        const outObj = this.fromGeneralObj(obj);
        return this.parseWellformedObject(outObj);
    }

    parseWellformedObject(sc) {
        const scoreExporter = new ScoreExporter(sc);
        scoreExporter.parse();
        return scoreExporter.asBytes();
    }

    fromGeneralObj(obj) {
        const classes = obj.classes;
        let outObj;
        for (const cM of _classMapping) {
            if (classes.includes(cM)) {
                const methName = 'from' + cM;
                outObj = this[methName](obj);
                break;
            }
        }
        if (outObj === undefined) {
            throw new MusicXMLExportException(`Cannot translate the object ${obj} to a complete musicXML document; put it in a Stream first!`);
        }
        return outObj;
    }

    fromScore(sc) {
        const scOut = sc.makeNotation({ inPlace: false });
        return scOut;
    }

    fromPart(p) {
        if (p.isFlat) {
            p = p.makeMeasures();
        }
        const s = new Score();
        s.insert(0, p);
        // metadata...;
        return this.fromScore(s);
    }

    fromMeasure(m) {
        const mCopy = m.makeNotation();
        if (m.clef === undefined) {
            mCopy.clef = clef.bestClef(mCopy, { recurse: true });
        }
        const p = new Part();
        p.append(mCopy);
        // TODO(msc): metadata;
        return this.fromPart(p);
    }

    fromVoice(v) {
        const m = new Measure();
        m.number = 1;
        m.insert(0, v);
        return this.fromMeasure(m);
    }

    // TODO(msc): fromStream
    // TODO(msc): fromDuration
    // TODO(msc): fromDynamic
    // TODO(msc): fromScale
    // TODO(msc): fromDiatonicScale
    // TODO(msc): fromMusic21Object

    fromGeneralNote(n) {
        const nCopy = n.clone(true);
        // makeTupletBrackets;
        const out = new Measure();
        out.number = 1;
        out.append(nCopy);

        return this.fromMeasure(out);
    }

    // TODO(msc): fromPitch
}

const _musicxmlVersion = '3.0';

/**
 * @memberOf music21.musicxml.m21ToXml
 */
export class XMLExporterBase {
    constructor() {
        this.doc = document.implementation.createDocument('', '', null);
        this.xmlRoot = undefined;
    }

    asBytes({ noCopy=true }={}) {
        let out = this.xmlHeader();
        const oSerializer = new XMLSerializer();
        out += oSerializer.serializeToString(this.xmlRoot);
        return out;
    }

    // no indentation :-(

    xmlHeader() {
        return `<?xml version="1.0" encoding="utf-8"?>
        <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML ${_musicxmlVersion}  Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
        `;
    }

    /**
     * Note: this is not a method in music21p, but it needs access to this.doc in music21j
     */
    _setTagTextFromAttribute(m21El, xmlEl, tag, attributeName, { transform, forceEmpty=false }={}) {
        if (attributeName === undefined) {
            attributeName = common.hyphenToCamelCase(tag);
        }

        let value = m21El[attributeName];
        if (transform !== undefined) {
            value = transform(value);
        }
        if ((value === undefined || value === '') && !forceEmpty) {
            return undefined;
        }
        const subElement = this.subElement(xmlEl, tag);
        if (value !== undefined) {
            subElement.innerHTML = value;
        }
        return subElement;
    }

    seta(m21El, xmlEl, tag, options) {
        return this._setTagTextFromAttribute(m21El, xmlEl, tag, options);
    }

    _setAttributeFromAttribute(m21El, xmlEl, xmlAttributeName, { attributeName, transform }={}) {
        if (attributeName === undefined) {
            attributeName = common.hyphenToCamelCase(xmlAttributeName);
        }
        let value = m21El[attributeName];
        if (value === undefined) {
            return;
        }
        if (transform !== undefined) {
            value = transform(value);
        }
        xmlEl.setAttribute(xmlAttributeName, value.toString());
    }

    setb(m21El, xmlEl, xmlAttributeName, options) {
        return this._setAttributeFromAttribute(m21El, xmlEl, xmlAttributeName, options);
    }

    // TODO(msc): _synchronizeIds;
    _synchronizeIds(element, m21Object) {}

    addDividerComment(comment='') {
        let commentLength = comment.length;
        if (commentLength > 60) {
            commentLength = 60;
        }
        const spacerLengthLow = Math.floor((60 - commentLength) / 2);
        const spacerLengthHigh = Math.ceil((60 - commentLength) / 2);
        const commentText = '='.repeat(spacerLengthLow) + ' ' + comment + ' ' + '='.repeat(spacerLengthHigh);
        const divider = this.doc.createComment(commentText);
        this.xmlRoot.appendChild(divider);
    }

    // TODO(msc): dump

    /**
     * Helper method since SubElement does not exist in javascript document.implementation
     */
    subElement(el, tag) {
        const subElement = this.doc.createElement(tag);
        el.appendChild(subElement);
        return subElement;
    }

    // TODO(msc): setStyleAttributes
    // TODO(msc): setTextFormatting
    // TODO(msc): setPrintStyleAlign
    // TODO(msc): setPrintStyle
    // TODO(msc): setPrintObject
    setColor(mxObject, m21Object) {
        if (m21Object.color !== undefined) {
            mxObject.setAttribute('color', normalizeColor(m21Object.color));
        } else if (m21Object.style !== undefined && m21Object.style.color !== undefined) {
            mxObject.setAttribute('color', normalizeColor(m21Object.style.color));
        }
    }

    // TODO(msc): setFont
    // TODO(msc): setPosition
    // TODO(msc): setEditorial
    setEditorial(mxEl, el) {

    }

    // TODO(msc): pageLayoutToXmlPrint
    // TODO(msc): pageLayoutToXmlPageLayout
    // TODO(msc): systemLayoutToXmlPrint
    // TODO(msc): systemLayoutToXmlSystemLayout
    // TODO(msc): staffLayoutToXmlStaffLayout

    accidentalToMx(a) {
        // TODO(msc): v 3.0 and v3.1 accidentals; microtone;
        let mxName;
        if (a.name === 'double-flat') {
            mxName = 'flat-flat';
        } else {
            mxName = a.name;
            // check other accidentals here.
        }
        const mxAccidental = this.doc.createElement('accidental');
        mxAccidental.innerHTML = mxName;
        // TODO(msc): parentheses, bracket, setPrintStyle
        return mxAccidental;
    }

    getRandomId() {
        // hack to get random ids.
        let text = '';
        const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

        for (let i = 0; i < 6; i++) {
            text += possible.charAt(Math.floor(Math.random() * possible.length));
        }

        return text;
    }

}

/**
 * @extends music21.musicxml.m21ToXml.XMLExporterBase
 */
export class ScoreExporter extends XMLExporterBase {
    constructor(score) {
        super();
        if (score === undefined) {
            this.stream = new Score();
        } else {
            this.stream = score;
        }
        this.xmlRoot = this.doc.createElement('score-partwise');
        this.xmlRoot.setAttribute('version', _musicxmlVersion);
        this.xmIdentification = undefined;
        this.scoreMetadata = undefined;
        this.spannerBundle = undefined;
        this.meterStream = undefined;
        this.scoreLayouts = undefined;
        this.firstScoreLayout = undefined;
        this.highestTime = 0.0;
        this.refStreamOrTimeRange = [0.0, this.highestTime];
        this.partExporterList = [];
        this.instrumentList = [];
        this.midiChannelList = [];
        this.parts = [];
    }

    parse() {
        const s = this.stream;
        if (s.length === 0) {
            return this.emptyObject();
        }
        this.scorePreliminaries();
        this.parsePartlikeScore(); // does not have parseFlatScore...
        this.postPartProcess();
        this.partExporterList = [];
        return this.xmlRoot;
    }

    emptyObject() {
        // TODO(msc): do this.
        return this.xmlRoot;
    }

    scorePreliminaries() {
        // this.setScoreLayouts();
        // this.setMeterStream();
        this.setPartsAndRefStream();
        // this.textBoxes = ...;
        this.highestTime = 0.0;
        // spannerBundle
    }

    setPartsAndRefStream() {
        const s = this.stream;
        const streamOfStreams = s.getElementsByClass('Stream');
        for (const innerStream of streamOfStreams) {
            // innerStream.transferOffsetToElements(); // only needed for appended Parts
            const ht = innerStream.highestTime;
            if (ht > this.highestTime) {
                this.highestTime = ht;
            }
            this.refStreamOrTimeRange = [0.0, this.highestTime];
        }
        this.parts = streamOfStreams;
    }

    // TODO(msc): setMeterStream
    // TODO(msc): setScoreLayouts

    parsePartlikeScore() {
        // makeRests
        for (const innerStream of this.parts) {
            const pp = new PartExporter(innerStream, { parent: this });
            // spanner bundle.
            pp.parse();
            this.partExporterList.push(pp);
        }
    }

    // TODO(msc): parseFlatScore

    postPartProcess() {
        this.setScoreHeader();
        for (let i = 0; i < this.partExporterList.length; i++) {
            const pex = this.partExporterList[i];
            this.addDividerComment('Part ' + i.toString());
            this.xmlRoot.appendChild(pex.xmlRoot);
        }
    }

    setScoreHeader() {
        // const s = this.stream;
        // scoreMeatadata
        // titles
        // identification
        // setDefaults
        // textBoxes
        this.setPartList();
    }

    // TODO(msc): textBoxToXmlCredit
    // TODO(msc): setDefaults
    // TODO(msc): addStyleToXmlDefaults
    // TODO(msc): styleToXmlAppearance

    setPartList() {
        // const spannerBundle = this.spannerBundle; // for now, always undefined;
        const mxPartList = this.subElement(this.xmlRoot, 'part-list');
        // staffGroups are non-existent
        for (const pex of this.partExporterList) {
            // const p = pex.stream;
            const mxScorePart = pex.getXmlScorePart();
            mxPartList.appendChild(mxScorePart);
        }
        return mxPartList;
    }
    // TODO(msc): staffGroupToXmlPartGroup;
    // TODO(msc): setIdentification
    // TODO(msc): metadataToMiscellaneous
    // TODO(msc): setEncoding
    // TODO(msc): getSupports
    // TODO(msc): setTitles
    // TODO(msc): contributorToXmlCreator
}

/**
 * @extends music21.musicxml.m21ToXml.XMLExporterBase
 */
export class PartExporter extends XMLExporterBase {
    constructor(partObj, { parent }={}) {
        super();
        this.stream = partObj;
        this.parent = parent;
        this.xmlRoot = this.doc.createElement('part');
        if (parent === undefined) {
            this.meterStream = new Stream();
            this.refStreamOrTimeRange = [0.0, 0.0];
            this.midiChannelList = [];
        } else {
            this.meterStream = parent.meterStream;
            this.refStreamOrTimeRange = parent.refStreamOrTimeRange;
            this.midiChannelList = parent.midiChannelList;
        }
        this.instrumentStream = undefined;
        this.firstInstrumentObject = undefined;

        this.lastDivisions = undefined;
        this.spannerBundle = partObj.spannerBundle;
        this.xmlPartId = this.getRandomId(); // hacky
    }

    parse() {
        // this.instrumentSetup();
        this.xmlRoot.setAttribute('id', this.xmlPartId);
        const measureStream = this.stream.getElementsByClass('Stream');
        // fixupNotation;
        // setIdLocals on spannerBundle;
        for (const m of measureStream) {
            this.addDividerComment('Measure ' + m.number.toString());
            const measureExporter = new MeasureExporter(m, { parent: this });
            measureExporter.spannerBundle = this.spannerBundle;
            const mxMeasure = measureExporter.parse();
            this.xmlRoot.appendChild(mxMeasure);
        }
        return this.xmlRoot;
    }

    // TODO(msc): instrumentSetup
    // TODO(msc): fixupNotationFlat -- might be redundant
    // TODO(msc): fixupNotationMeasured

    getXmlScorePart() {
        // const part = this.stream;
        const mxScorePart = this.doc.createElement('score-part');
        mxScorePart.setAttribute('id', this.xmlPartId);
        // partName
        // partAbbreviation
        // instrument
        return mxScorePart;
    }
    // TODO(msc): instrumentToXmlScoreInstrument
    // TODO(msc): instrumentToXmlMidiInstrument
}

const _classesToMeasureMethods = [
    ['Note', 'noteToXml'],
    // NoChord
    // ChordWithFretBoard
    // ChordSymbol
    ['Chord', 'chordToXml'],
    ['Rest', 'restToXml'],
    // Dynamic, Segno, Coda, MetronomeMark, MetricModulation,
    // TextExpression, RepeatExpression, RehersalMark
];

const _wrapAttributeMethodClasses = [
    ['Clef', 'clefToXml'],
    ['KeySignature', 'keySignatureToXml'],
    ['TimeSignature', 'timeSignatureToXml'],
];

const _ignoreOnParseClasses = ['LayoutBase', 'Barline'];

const divisionsPerQuarter = 32 * 3 * 3 * 5 * 7; // TODO(msc): create defaults.js

/**
 * @extends music21.musicxml.m21ToXml.XMLExporterBase
 */
export class MeasureExporter extends XMLExporterBase {
    constructor(measureObj, { parent }={}) {
        super();
        this.stream = measureObj;
        this.parent = parent;
        this.xmlRoot = this.doc.createElement('measure');
        this.currentDivisions = divisionsPerQuarter;
        this.transpositionInterval = undefined;
        this.mxTranspose = undefined;
        this.measureOffsetStart = 0.0;
        this.offsetInMeasure = 0.0;
        this.currentVoiceId = undefined;

        this.rbSpanners = [];
        this.spannerBundle = parent.spannerBundle;

        this.objectSpannerBundle = this.spannerBundle;
    }

    parse() {
        // TODO(msc): setTranspose
        // TODO(msc): setRbSpanners
        this.setMxAttributes();
        // TODO(msc): setMxPrint
        this.setMxAttributesObjectForStartOfMeasure();
        // TODO(msc): setLeftBarline

        // THE BIG ONE
        this.mainElementsParse();

        // TODO(msc): setRightBarline
        return this.xmlRoot;
    }

    mainElementsParse() {
        const m = this.stream;
        if (!m.hasVoices()) {
            this.parseFlatElements(m, { backupAfterwards: false });
            return;
        }
        // TODO(msc): parse elements outside of Voices...needs getElementsNotOfClass
        const allVoices = Array.from(m.voices);
        for (const [i, v] of allVoices.entries()) {
            let backupAfterwards = true;
            if (i === allVoices.length - 1) {
                backupAfterwards = false;
            }
            this.parseFlatElements(v, { backupAfterwards });
        }
    }

    parseFlatElements(m, { backupAfterwards=false }={}) {
        const root = this.xmlRoot;
        const divisions = this.currentDivisions;
        this.offsetInMeasure = 0.0;
        let voiceId;
        if (m.classes.includes('Voice')) {
            voiceId = m.id;
            if (voiceId === undefined) {
                voiceId = this.getRandomId();
            }
        }
        this.currentVoiceId = voiceId;

        for (const el of m) {
            this.parseOneElement(el);
        }

        if (backupAfterwards) {
            const amountToBackup = Math.round(divisions * this.offsetInMeasure);
            if (amountToBackup > 0) {
                const mxBackup = this.doc.createElement('backup');
                const mxDuration = this.subElement(mxBackup, 'duration');
                mxDuration.innerHTML = amountToBackup.toString();
                root.appendChild(mxBackup);
            }
        }
        this.currentVoiceId = undefined;
    }

    parseOneElement(obj) {
        // const root = this.xmlRoot;
        // spanners...
        const classes = obj.classes;
        if (classes.includes('GeneralNote')) {
            this.offsetInMeasure += obj.duration.quarterLength;
        }
        // odd durations...

        let parsedObject = false;

        for (const [className, methName] of _classesToMeasureMethods) {
            if (classes.includes(className)) {
                this[methName](obj);
                parsedObject = true;
                break;
            }
        }

        for (const [className, methName] of _wrapAttributeMethodClasses) {
            if (classes.includes(className)) {
                const meth = o => this[methName](o);
                this.wrapObjectInAttributes(obj, meth);
                parsedObject = true;
                break;
            }
        }

        // deal with skipped objects.
        if (!parsedObject && !_ignoreOnParseClasses.includes(obj.classes[0])) {
            console.warn('skipped object of class ' + obj.classes[0]);
        }

        // postSpanners.
    }

    // TODO(msc): prePostObjectSpanners
    // TODO(msc): _spannerStartParameters
    // TODO(msc): _spannerEndParameters
    // TODO(msc): objectAttachedSpaners

    /**
     *
     * @param {music21.note.GeneralNote} n
     * @param noteIndexInChord
     * @param chordParent
     * @returns {Node}
     */
    noteToXml(n, { noteIndexInChord=0, chordParent }={}) {
        const addChordTag = (noteIndexInChord !== 0);
        let chordOrN;
        if (chordParent === undefined) {
            chordOrN = n;
        } else {
            chordOrN = chordParent;
        }
        const mxNote = this.doc.createElement('note');
        // setPrintStyle
        // volumeInformation
        this.setColor(mxNote, n);
        // _synchronizeId;
        const d = chordOrN.duration;
        // grace;
        // setColor chord
        // setPrintObject
        // hideObject
        // articulation pizz:
        if (addChordTag) {
            this.subElement(mxNote, 'chord');
        }
        if (n.pitch !== undefined) {
            const mxPitch = this.pitchToXml(n.pitch);
            mxNote.appendChild(mxPitch);
        } else {
            this.subElement(mxNote, 'rest');
        }
        if (d.isGrace !== true) {
            const mxDuration = this.durationXml(d);
            mxNote.appendChild(mxDuration);
        }
        if (n.tie !== undefined) {
            const mxTieList = this.tieToXmlTie(n.tie);
            for (const t of mxTieList) {
                mxNote.appendChild(t);
            }
        }
        // instrument
        this.setEditorial(mxNote, n);
        if (this.currentVoiceId !== undefined) {
            const mxVoice = this.subElement(mxNote, 'voice');
            let vId;
            if (typeof vId === 'number') {
                vId = this.currentVoiceId + 1;
            } else {
                // not a number;
                vId = this.currentVoiceId;
            }
            mxVoice.innerHTML = vId.toString();
        }

        const mxType = this.subElement(mxNote, 'type');
        mxType.innerHTML = typeToMusicXMLType(d.type);
        // set styleAttributes
        // set noteSize
        for (let _ = 0; _ < d.dots; _++) {
            this.subElement(mxNote, 'dot');
        }

        // components.
        if (n.pitch !== undefined
                && n.pitch.accidental !== undefined
                && n.pitch.accidental.displayStatus !== false) {
            const mxAccidental = this.accidentalToMx(n.pitch.accidental);
            mxNote.appendChild(mxAccidental);
        }
        if (d.tuplets.length > 0) {
            // todo--nested tuplets;
            const mxTimeModification = this.tupletToTimeModification(d.tuplets[0]);
            mxNote.appendChild(mxTimeModification);
        }

        let stemDirection;
        if (!addChordTag
                && ![undefined, 'unspecified'].includes(chordOrN.stemDirection)) {
            stemDirection = chordOrN.stemDirection;
        } else if (chordOrN !== n
                && ![undefined, 'unspecified'].includes(n.stemDirection)) {
            stemDirection = n.stemDirection;
        }
        if (stemDirection !== undefined) {
            const mxStem = this.subElement(mxNote, 'stem');
            let sdtext = stemDirection;
            if (sdtext === 'noStem') {
                sdtext = 'none';
            }
            mxStem.innerHTML = sdtext;
            // TODO: stemStyle
        }

        // dealWithNotehead
        // beams
        // staff
        // notations
        // tuplet display
        // notations
        if (!addChordTag) {
            for (const lyricObj of chordOrN.lyrics) {
                if (lyricObj.text === undefined) {
                    continue;
                }
                const mxLyric = this.lyricToXml(lyricObj);
                mxNote.appendChild(mxLyric);
            }
        }

        this.xmlRoot.appendChild(mxNote);
        return mxNote;
    }

    restToXml(r) {
        return this.noteToXml(r);
        // full measure
        // display-step, display-octave, etc.
    }

    chordToXml(c) {
        const mxNoteList = [];
        for (const [i, n] of Array.from(c).entries()) {
            const mxNote = this.noteToXml(n, { noteIndexInChord: i, chordParent: c });
            mxNoteList.push(mxNote);
        }
        return mxNoteList;
    }

    durationXml(dur) {
        const mxDuration = this.doc.createElement('duration');
        mxDuration.innerHTML = Math.round(this.currentDivisions * dur.quarterLength).toString();
        return mxDuration;
    }

    pitchToXml(p) {
        const mxPitch = this.doc.createElement('pitch');
        this._setTagTextFromAttribute(p, mxPitch, 'step');
        if (p.accidental !== undefined) {
            const mxAlter = this.subElement(mxPitch, 'alter');
            mxAlter.innerHTML = common.numToIntOrFloat(p.accidental.alter).toString();
        }
        this._setTagTextFromAttribute(p, mxPitch, 'octave', 'implicitOctave');
        return mxPitch;
    }
    // TODO(msc): fretNoteToXml
    // TODO(msc): fretBoardToXml
    // TODO(msc): chordWithFretBoardToXml

    tupletToTimeModification(tup) {
        const mxTimeModification = this.doc.createElement('time-modification');
        this._setTagTextFromAttribute(tup, mxTimeModification, 'actual-notes', 'numberNotesActual');
        this._setTagTextFromAttribute(tup, mxTimeModification, 'normal-notes', 'numberNotesNormal');
        if (tup.durationNormal !== undefined) {
            const mxNormalType = this.subElement(mxTimeModification, 'normal-type');
            mxNormalType.innerHTML = typeToMusicXMLType(tup.durationNormal.type);
            if (tup.durationNormal.dots > 0) {
                for (let i = 0; i < tup.durationNormal.dots; i++) {
                    this.subElement(mxTimeModification, 'normal-dot');
                }
            }
        }
        return mxTimeModification;
    }

    // TODO(msc): dealWithNotehead
    // TODO(msc): noteheadToXml
    // TODO(msc): noteToNotations

    tieToXmlTie(t) {
        const mxTieList = [];
        let musicxmlTieType = t.type;
        if (t.type === 'continue') {
            musicxmlTieType = 'stop';
        }
        const mxTie = this.doc.createElement('tie');
        mxTie.setAttribute('type', musicxmlTieType);
        mxTieList.push(mxTie);

        if (t.type === 'continue') {
            const mxTie = this.doc.createElement('tie');
            mxTie.setAttribute('type', 'start');
            mxTieList.push(mxTie);
        }
        return mxTieList;
    }

    // TODO(msc): tieToXmlTied -- needs notations
    // TODO(msc): tupletToXmlTuplet
    // TODO(msc): expressionToXml
    // TODO(msc): articulationToXmlArticulation
    // TODO(msc): setLineStyle
    // TODO(msc): articulationToXmlTechnical
    // TODO(msc): setHarmonic
    // TODO(msc): noChordToXml
    // TODO(msc): chordSymbolToXml
    // TODO(msc): setOffsetOptional
    // TODO(msc): placeInDirection
    // TODO(msc): dynamicToXml
    // TODO(msc): segnoToXml
    // TODO(msc): codaToXml
    // TODO(msc): tempoIndicationToXml
    // TODO(msc): rehearsalMarkToXml
    // TODO(msc): textExpressionToXml

    wrapObjectInAttributes(objectToWrap, methodToMx) {
        if (this.offsetInMeasure === 0.0) {
            return undefined;
        }

        const mxAttributes = this.doc.createElement('attributes');
        const mxObj = methodToMx(objectToWrap);
        mxAttributes.appendChild(mxObj);
        this.xmlRoot.appendChild(mxAttributes);
        return mxAttributes;
    }

    lyricToXml(l) {
        const mxLyric = this.doc.createElement('lyric');
        this._setTagTextFromAttribute(l, mxLyric, 'syllabic');
        this._setTagTextFromAttribute(l, mxLyric, 'text', 'text', { forceEmpty: true });
        if (l.identifier !== undefined) {
            mxLyric.setAttribute('name', l.identifier.toString());
        }

        if (l.number !== undefined) {
            mxLyric.setAttribute('number', l.number.toString());
        } else if (l.identifier !== undefined) {
            mxLyric.setAttribute('number', l.identifier.toString());
        }
        // setStyleAttributes
        // setPrintObject
        // setColor
        // setPosition
        return mxLyric;
    }
    // TODO(msc): beamsToXml
    // TODO(msc): beamToXml
    // TODO(msc): setRightBarline
    // TODO(msc): setLeftBarline
    // TODO(msc): setBarline
    // TODO(msc): barlineToXml
    // TODO(msc): repeatToXml

    setMxAttributesObjectForStartOfMeasure() {
        const m = this.stream;
        const mxAttributes = this.doc.createElement('attributes');
        let appendToRoot = false;
        this.currentDivisions = divisionsPerQuarter;
        if (this.parent === undefined || this.currentDivisions !== this.parent.lastDivisions) {
            const mxDivisions = this.subElement(mxAttributes, 'divisions');
            mxDivisions.innerHTML = this.currentDivisions.toString();
            this.parent.lastDivisions = this.currentDivisions;
            appendToRoot = true;
        }
        if (m.classes.includes('Measure')) {
            if (m._keySignature !== undefined) {
                mxAttributes.appendChild(this.keySignatureToXml(m._keySignature));
                appendToRoot = true;
            }
            if (m._timeSignature !== undefined) {
                mxAttributes.appendChild(this.timeSignatureToXml(m._timeSignature));
                appendToRoot = true;
            }
            // todo SenzaMisura...
            if (m._clef !== undefined) {
                mxAttributes.appendChild(this.clefToXml(m._clef));
                appendToRoot = true;
            }
        }

        // staffLayout
        // transpositionInterval
        // measureStyle
        if (appendToRoot) {
            this.xmlRoot.appendChild(mxAttributes);
        }
        return mxAttributes;
    }
    // TODO(msc): measureStyle
    // TODO(msc): staffLayoutToXmlStaffDetails

    timeSignatureToXml(ts) {
        const mxTime = this.doc.createElement('time');
        // synchronizeIds
        // senzaMisura
        // summed denominators, compound etc.
        const mxBeats = this.subElement(mxTime, 'beats');
        mxBeats.innerHTML = ts.numerator.toString();
        const mxBeatType = this.subElement(mxTime, 'beat-type');
        mxBeatType.innerHTML = ts.denominator.toString();
        // symbolizeDenominator
        // separator
        // style
        return mxTime;
    }

    keySignatureToXml(keyOrKeySignature) {
        const mxKey = this.doc.createElement('key');
        // synchronizeIds
        // number
        // printStyle, print-object
        this.seta(keyOrKeySignature, mxKey, 'fifths', 'sharps');
        if (keyOrKeySignature.mode !== undefined) {
            this.seta(keyOrKeySignature, mxKey, 'mode');
        }
        // non-traditional
        // altered pitches
        return mxKey;
    }

    clefToXml(clefObj) {
        const mxClef = this.doc.createElement('clef');
        // printstyle
        const sign = clefObj.sign || 'G';
        const mxSign = this.subElement(mxClef, 'sign');
        mxSign.innerHTML = sign;
        this.seta(clefObj, mxClef, 'line');
        if (clefObj.octaveChange !== undefined && clefObj.octaveChange !== 0) {
            this.seta(clefObj, mxClef, 'clef-octave-change', 'octaveChange');
        }
        return mxClef;
    }

    // intervalToXmlTranspose
    // setMxPrint
    // staffLayoutToXmlPrint
    setMxAttributes() {
        const m = this.stream;
        this.xmlRoot.setAttribute('number', m.measureNumberWithSuffix());
        // layoutWidth
    }

    // setRbSpanners
    // transpose
}
Music21j, Copyright © 2013-2021 Michael Scott Asato Cuthbert.
Documentation generated by JSDoc 3.6.3 on Wed Jul 31st 2019 using the DocStrap template.