Source

vendors/P5/sketchFeedback.js

// File imports
import atVars from "../AlphaTab/variables";
import * as sketchBehaviors from "./sketchBehaviors";

/**
 * P5 sketch wrapper for real time feedback
 * @module sketchFeedback
 * @category P5
 * @author Daniel Griessler <dgriessler20@gmail.com>
 * @author Dan Levy <danlevy124@gmail.com>
 */

/**
 * Wrapper for local p5 setup and draw functions
 * @function
 * @param {sketch} p Sketch object that will include all of the functions that will be called by p5
 */
const p5FeedbackSketch = (p) => {
    p.type = sketchBehaviors.REAL_TIME_FEEDBACK;
    // how much space to add around note for drawing lines, obtained by guess and check
    const EXTRA_BAR_VARIANCE = 7;

    // document elements retrieved from the document
    let barCursor;
    let alphaTabSurface;
    let sideNavElementWidth;

    // wraps together the Alpha tab container and p5's canvas
    let wrapper;

    // provided by reference which is updated in other functions
    let drawer;

    // reference to p5's canvas when created. It should overlay the AlphaTab canvas
    let canvas;

    // how large to draw the notehead
    // TODO: Dynamically change based on scale of music
    let circleSize = 10;

    // stores last two XY coordinates of notes drawn
    let previousPos = [-1, -1, -1, -1];

    // stores the last recorded midi value and its time
    let lastPitchAndTime = [-1, -1];

    /**
     * This function is called twice. Once, upon initialization p5 calls it which we use to tell p5 to stop looping
     * Then, AlphaTab will call setup when its done being rendered. Then, the canvas can be setup for drawing since
     * the canvas overlays the AlphaTab container
     * @param {Drawer} drawerGiven - p5 will not provide this but atVars provides a reference to the Drawer being used
     */
    p.setup = function (drawerGiven) {
        if (drawerGiven === undefined) {
            p.noLoop();
            return;
        }
        drawer = drawerGiven;
        // Retrieved by attaching unique IDs to elements generated by AlphaTab. Required editing AlphaTab.js directly
        barCursor = document.getElementById("bC");
        alphaTabSurface = document.getElementById("aTS");
        const sideNavElement = document.querySelector("#side-nav");
        sideNavElementWidth =
            sideNavElement !== null ? sideNavElement.clientWidth : 0;
        wrapper = document.getElementById("alpha-tab-wrapper");

        // creates a canvas that overlaps the alphaTabSurface. Position is absolute for the canvas by default
        canvas = p.createCanvas(
            alphaTabSurface.clientWidth,
            alphaTabSurface.clientHeight
        );
        const x = 0;
        const y = 0;
        canvas.position(x, y);
        canvas.parent("sketch-holder");
    };

    /**
     * Updates the drawer line heights from the page if the drawer is active
     * @function
     */
    const updateDrawerLines = () => {
        let topLine = document.getElementById("rect_0");
        let nextLine = document.getElementById("rect_1");
        if (
            drawer &&
            topLine &&
            topLine.y &&
            topLine.y.animVal &&
            topLine.y.animVal.value &&
            nextLine &&
            nextLine.y &&
            nextLine.y.animVal &&
            nextLine.y.animVal.value
        ) {
            const topLineHeight = topLine.y.animVal.value;
            const distanceBetweenLines =
                nextLine.y.animVal.value - topLineHeight;
            drawer.setTopLineAndDistanceBetween(
                topLineHeight,
                distanceBetweenLines,
                drawer.baseOctave
            );
        }
    };

    /**
     * Draws the canvas on the screen. Requires that the canvas is not undefined ie setup has run
     * TODO: Handle sheet music scale
     */
    p.draw = function () {
        if (!atVars.getsFeedback) {
            return;
        }

        if (atVars && atVars.shouldResetDrawPositions) {
            previousPos[0] = -1;
            previousPos[1] = -1;
            previousPos[2] = -1;
            previousPos[3] = -1;
            atVars.shouldResetDrawPositions = false;
            barCursor = document.getElementById("bC");
            alphaTabSurface = document.getElementById("aTS");
        }

        // handles clearing ahead and drawing line behind the note head
        if (previousPos[0] !== -1 && previousPos[1] !== -1) {
            // fills with white
            p.fill("#F8F8F8");

            // draws clearing rectangle with total height of alpha tab from previous X position to the end
            p.rect(
                previousPos[0],
                0,
                alphaTabSurface.clientWidth - previousPos[0],
                alphaTabSurface.clientHeight
            );
    
            if (previousPos[2] !== -1) {
                // If there is any confusion in the player then this will help keep the drawing at the right height
                if (previousPos[0] < previousPos[2]) {
                    updateDrawerLines();
                }
            }

            if (previousPos[2] !== -1) {
                // If there is any confusion in the player then this will help keep the drawing at the right height
                if (previousPos[0] < previousPos[2]) {
                    updateDrawerLines();
                }
            }

            // don't draw silence which has special value -1 or if we don't have a previous point
            if (
                drawer &&
                drawer.note.midiVal >= 0 &&
                previousPos[2] !== -1 &&
                previousPos[3] !== -1
            ) {
                if (atVars.noteStream[atVars.noteStreamIndex] === -1) {
                    // singing should be silent
                    p.stroke(255, 0, 0);
                } else {
                    let diff = Math.abs(
                        lastPitchAndTime[0] -
                            atVars.noteStream[atVars.noteStreamIndex]
                    );

                    // fill with green if really close
                    if (diff < 1) {
                        p.stroke(0, 255, 0);
                    } else if (diff < 2) {
                        // yellow if farther away
                        p.stroke("#CCCC00");
                    } else {
                        // and red if too far or singing when should be silent
                        p.stroke(255, 0, 0);
                    }
                }

                p.strokeWeight(3);
                p.line(
                    previousPos[0],
                    previousPos[1],
                    previousPos[2],
                    previousPos[3]
                );
                p.noStroke();
            }
            previousPos[2] = previousPos[0];
            previousPos[3] = previousPos[1];
        }

        // dont draw the outline of the shape, note: you need to turn stroke on to draw lines as we do below.
        p.noStroke();

        if (drawer) {
            lastPitchAndTime[0] = drawer.note.midiVal;
            lastPitchAndTime[1] = atVars.api.timePosition / 1000;
            while (
                lastPitchAndTime[1] >
                atVars.cumulativeTime +
                    atVars.noteStream[atVars.noteStreamIndex + 1]
            ) {
                atVars.cumulativeTime +=
                    atVars.noteStream[atVars.noteStreamIndex + 1];
                atVars.noteStreamIndex += 2;
            }

            let currentHeight = drawer.noteHeight;
            previousPos[1] = currentHeight;

            // fills with pink
            p.fill(255, 0, 255);

            // Binds x position to the bar cursor
            let posX =
                barCursor.getClientRects()[0].left.valueOf() -
                sideNavElementWidth +
                wrapper.scrollLeft -
                27;

            // TODO: Handle resizing scale
            // places sharp if present beside the note. These magic values were calculated via trial and error
            let sharpPos = [posX - 14, currentHeight + 3.5];
            previousPos[0] = sharpPos[0] - EXTRA_BAR_VARIANCE;

            // silence has Sentinel value -1 so only draw when not silent
            if (drawer.note.midiVal >= 0) {
                // actually draws note circle at the given position
                p.ellipse(posX, currentHeight, circleSize, circleSize);

                // TODO Handle resizing scale
                // Adds sharp symbol if needed
                if (drawer.note.isSharp) {
                    p.text("#", sharpPos[0], sharpPos[1]);
                }

                // Adds ledger lines above or below the staff
                if (drawer.belowOrAbove !== 0) {
                    let isIncreasing = drawer.belowOrAbove > 0;
                    p.stroke(0);
                    p.strokeWeight(1);
                    let height = isIncreasing
                        ? drawer.topLine
                        : drawer.firstLine - drawer.distanceBetweenLines;
                    for (let i = 0; i < Math.abs(drawer.belowOrAbove); i++) {
                        if (isIncreasing) {
                            height -= drawer.distanceBetweenLines;
                        } else {
                            height += drawer.distanceBetweenLines;
                        }
                        p.line(
                            posX - EXTRA_BAR_VARIANCE,
                            height,
                            posX + EXTRA_BAR_VARIANCE,
                            height
                        );
                    }
                    p.noStroke();
                }
            }
        }
    };
};

export default p5FeedbackSketch;