Source

vendors/P5/Drawer.js

// File imports
import { getNumberOfLedgerLines, getOctave } from "../AlphaTab/ledgerLines";

/**
 * @class
 * @classdesc Keeps track of current note and where to draw it on the screen along with special information such as number of extra ledger lines
 * @category P5
 * @author Daniel Griessler <dgriessler20@gmail.com>
 */
class Drawer {
    /**
     * Creates a new Drawer setting up storage of the most recent midi note and information about how to draw it on the screen
     * @param {Number} topLine Height of the top line of the selected part to sing
     * @param {Number} distanceBetweenLines Distance between lines in the staff
     */
    constructor(topLine, distanceBetweenLines, baseOctave) {
        this.topLine = topLine;
        this.distanceBetweenLines = distanceBetweenLines;
        // stores the height of the lowest line of the staff being sung
        this.firstLine = this.topLine + this.distanceBetweenLines * 5;
        // Values >= selected lower limit and <= selected upper limit don't need extra ledger lines
        this.lowerLimit = 61; // 61 = C4#
        this.upperLimit = 81; // 81 = A5
        this.lowerLimit2 = 40; // 40 = E2
        this.upperLimit2 = 60; // 60 = C4
        this.note = new Note(60);
        this.belowOrAbove = 0;
        this.noteHeight = 0;
        this.baseOctave = baseOctave;
        this.updateNote(this.note.midiVal);
    }

    /**
     * Sets the stored height of the top line, the distance between ledger lines, and the base octave
     * @param {number} topLine The height of the top ledger line
     * @param {number} distanceBetweenLines The y distance between ledger lines
     * @param {number} baseOctave The base octave of the current clef
     */
    setTopLineAndDistanceBetween(topLine, distanceBetweenLines, baseOctave) {
        this.topLine = topLine + 1;
        this.distanceBetweenLines = distanceBetweenLines;
        // stores the height of the lowest line of the staff being sung
        this.firstLine =
            this.topLine +
            this.distanceBetweenLines * (baseOctave === 4 ? 5 : 6);
        this.baseOctave = baseOctave;
    }

    /**
     * Sets the base octave
     * @param {number} baseOctave The base octave of the current clef
     */
    setBaseOctave(baseOctave) {
        this.baseOctave = baseOctave;
    }

    /**
     * Updates the Drawer to the new provided note
     * @param {Number} note New midi value to store. Provide a -1 as a sentinel value for silence
     */
    updateNote(note) {
        this.note.updateNote(note);
        this.getHeightOfNote();
        this.getExtraFeatures();
    }

    /**
     * Updates the height of the note based on its midi value
     */
    getHeightOfNote() {
        // -1 is a sentinel value for silence which is assigned a default height
        if (this.note.midiVal === -1) {
            this.noteHeight = this.firstLine;
            return;
        }
        // Calculating the height of a note relies on the cycle in musical notes that occurs between octaves
        // This calculates what the height of the note should be based on the first line
        const heightMod = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];

        // based on the starting note subtract the base octave since the start note is in that given octave
        let octaveMod = this.note.octave - this.baseOctave;
        let value = heightMod[this.note.midiVal % heightMod.length];

        // Includes bump to jump between octaves
        let totalMod = value + octaveMod * 7;

        // final height includes division by 2 because each value in the totalMod is distanceBetweenLines/2
        this.noteHeight =
            this.firstLine - (totalMod * this.distanceBetweenLines) / 2;
    }

    /**
     * Gets the extra features of a note including how many ledger lines to add
     */
    getExtraFeatures() {
        // -1 is a sentinel value for silence which has no ledger lines
        if (this.note.midiVal === -1) {
            this.belowOrAbove = 0;
            return;
        }

        // // similar to note height, there's a cycle between octaves for ledger lines
        // const aboveBelowMod = [1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4];

        // sets up base if ledger lines are even needed. base == 0 means no ledger lines
        // base < 0 means they go below the staff, base > 0 means they go above the staff
        let base = 0;
        let actualUpperLimit;
        let actualLowerLimit;
        if (this.baseOctave === 4) {
            actualUpperLimit = this.upperLimit;
            actualLowerLimit = this.lowerLimit;
        } else {
            actualUpperLimit = this.upperLimit2;
            actualLowerLimit = this.lowerLimit2;
        }
        if (this.note.midiVal >= actualUpperLimit) {
            base = actualUpperLimit;
        } else if (this.note.midiVal <= actualLowerLimit) {
            base = -1 * actualLowerLimit;
        }

        // If need ledger lines, then calculate how many are required
        if (base !== 0) {
            let direction;
            let start;
            if (base > 0) {
                direction = "up";
                start = this.baseOctave === 4 ? "a" : "c";
            } else {
                direction = "down";
                start = this.baseOctave === 4 ? "c" : "e";
            }

            // LedgerLines loop every 24 notes so if more than 24 then add 7 octaves
            let difference = Math.abs(Math.abs(base) - this.note.midiVal);
            let loopAdd = 7 * Math.floor(difference / 24);
            this.belowOrAbove =
                getNumberOfLedgerLines(this.note.midiVal, direction, start) +
                loopAdd;

            // Signals to draw ledger lines below staff
            if (base < 0) {
                this.belowOrAbove *= -1;
            }
        } else {
            this.belowOrAbove = 0;
        }
    }
}

/**
 * @class
 * @category P5
 * @classdesc Stores midi value as its character representation including its octave and if it is sharp
 */
class Note {
    /**
     * Constructs a Note from a provided a given midiVal and converts it to a string which can be accessed
     * @param {Number} midiVal Midi value of note to store
     */
    constructor(midiVal) {
        this.updateNote(midiVal);
    }

    /**
     * Updates the note stored to the new note
     * @param {Number} note New midi value to store
     */
    updateNote(note) {
        // No point in updating if the midi value matches the current one
        if (this.midiVal && note === this.midiVal) {
            return;
        }

        this.midiVal = note;
        const noteText = this.numToNote();
        this.charPart = noteText.charPart;
        this.octave = noteText.octave;

        // relies on the char part with being a single letter like G or two letters which is the note and # for sharp
        this.isSharp = this.charPart.length === 2;
    }

    /**
     * @typedef {object} NotePackage
     * @memberof Drawer
     * @property {string} charPart The character part of the note
     * @property {number} octave The octave of the note
     */

    /**
     * Converts the stored midi value to its character representation
     * @returns {NotePackage} A tuple with the character part and the octave
     */
    numToNote() {
        let charPart;
        let octave;

        // -1 is a sentinel value for silence which has no char part or octave
        if (this.midiVal === -1) {
            charPart = "-";
            octave = "";
        } else {
            const letters = [
                "C",
                "C#",
                "D",
                "D#",
                "E",
                "F",
                "F#",
                "G",
                "G#",
                "A",
                "A#",
                "B",
            ];
            charPart = letters[this.midiVal % letters.length];
            octave = getOctave(this.midiVal);
        }
        return { charPart, octave };
    }
}

export default Drawer;