Source: AlphaTab/Note.js

const BASE_MIDI_OCTAVE_ZERO = 21; // octave zero is special because it doesn't follow the octave pattern
const BASE_MIDI_OCTAVE_ONE = 24; // octave one starts the normal octave pattern
const NUM_HALF_BETWEEN_OCTAVE = 12; // number of half steps in an octave
/**
 * @class
 * @classdesc Encapsulates each note in the music composed of the string part of the note, octave, duration in alphaTex, set of beateffects, set of noteeffects
 */
class Note {
    /**
     * Creates a new Note
     */
    constructor() {
        this.note = "";
        this.octave = -1;
        this.duration = -1;
        this.beatEffects = undefined;
        this.noteEffects = undefined;
    }

    /**
     * Sets the string part of the note
     * @param {string} note string part of a note including sharps (#) and flats (b)
     */
    setNote(note) {
        this.note = note;
    }

    /**
     * Sets the octave of the note
     * @param {number} octave octave of the note to be set
     */
    setOctave(octave) {
        this.octave = octave;
    }

    /**
     * Sets duration of the note based on alphaTex
     * @param {number} duration duration of the note in alphatex (1,2,4,8,16, etc.)
     */
    setDuration(duration) {
        this.duration = duration;
    }

    /**
     * Adds beat effect to set
     * @param {string} effect effect to be added
     */
    addBeatEffect(effect) {
        if (this.beatEffects === undefined) {
            this.beatEffects = new Set();
        }

        this.beatEffects.add(effect);
    }

    /**
     * Copies beat effects from source
     * @param {object} source Source must have getInt, getStr, attributesBool
     * @property {function} source.getInt Provides a number matched with the provided String else undefined
     * @property {function} source.getStr Provides a String matched with the provided String else undefined
     * @property {Set} source.attributesBool Set with String attributes
     */
    copyBeatEffects(source) {
        if (source.getInt("tuplet")) {
            this.addBeatEffect("tuplet " + source.getInt("tuplet").toString());
        }
        if (source.getStr("dynamic")) {
            this.addBeatEffect("dynamic " + source.getStr("dynamic").toString());
        }
        source.attributesBool.forEach((value1, value2, set) => {
            this.addBeatEffect(value1);
        });
    }

    /**
     * Checks if beateffects contains the effect
     * @param {string} effect String of effect to check for
     * @returns {boolean} boolean if the effect is in the set of beat effects
     */
    containsBeatEffect(effect) {
        return this.beatEffects !== undefined && this.beatEffects.has(effect);
    }

    /**
     * Gets offset of note from base value based on string part stored in this.note
     * @returns {number} The offset of the note
     */
    getNoteOffset() {
        let base;
        switch(this.note[0]) {
            case 'c': base = 0;
            break;
            case 'd': base = 2;
            break;
            case 'e': base = 4;
            break;
            case 'f': base = 5;
            break;
            case 'g': base = 7;
            break;
            case 'a': base = 9;
            break;
            case 'b': base = 11;
            break;
            default:
                base = -1;
        }

        for (let i =  1; i < this.note.length; i++) {
            if (this.note[i] === '#') {
                base++;
            } else if (this.note[i] === 'b') {
                base--;
            }
        }

        return base;
    }

    /**
     * Calculates and returns the midi value of the stored string representation of the note with the stored octave
     * @returns {number} The midi value of the stored note or -1 if no octave
     */
    getMidiValue() {
        if (this.octave < 0) {
            return -1;
        }

        let midi = this.octave == 0 ? BASE_MIDI_OCTAVE_ZERO : BASE_MIDI_OCTAVE_ONE;
        let adjustedOctave = this.octave == 0 ? 0 : this.octave - 1;

        return midi + NUM_HALF_BETWEEN_OCTAVE * adjustedOctave + this.getNoteOffset();
    }

    /**
     * Gets the duration in seconds of the stored note
     * @param {number} currentTempoFactor the current tempo factor affecting the length of the note
     * @returns {number} The duration in seconds
     */
    getDurationInSeconds(currentTempoFactor) {
        let durationReal = 4 / this.duration;
        let durationInSeconds = currentTempoFactor * durationReal;
        if (this.beatEffects !== undefined && this.beatEffects.has("dotted")) {
            durationInSeconds *= 1.5;
        }
        return durationInSeconds;
    }

    /**
     * Converts Note to a pretty string
     * @returns {string} The pretty string representation of the note
     */
    toString() {
        let output = "";

        output += this.note;

        if (this.octave > 0) {
            output += this.octave.toString(10);
        }

        if (this.beatEffects !== undefined && this.beatEffects.size > 0) {
            output += "{";
            this.beatEffects.forEach((value1, value2, set) => {
                output += " ";
                output += value1;
            });
            output += " }";
        }
        output += ".";
        output += this.duration.toString(10);
        if (this.noteEffects != undefined && this.noteEffects.size > 0) {
            output += "{";
            this.noteEffects.forEach((value1, value2, set) => {
                output += " ";
                output += value1;
            });
            output += " }";
        }

        return output;
    }
}

module.exports = Note