Source: Parser/PerformanceAnalyser.js

const ParserPData = require("./ParserPerformanceData.js");
const ParsedOutputManipulator = require("./ParsedOutputManipulator.js");

/**
 * @class
 * @classdesc Encapsulates a function for analyzing a performance
 */
class PerformanceAnalyser {

    /**
     * @typedef {object} AnalyzedPerformancePackage
     * @property {number} durationDifference The calculated error in duration for that measure (big is bad)
     * @property {number} pitchDifference The calculated score in pitch for the that measure (big is good)
     * @property {number} followBadMeasures The number of measures following this one that are worse than it
     * @property {number} overallScore Scaled overall score for that measure (100 is perfect)
     * @property {number} measureNumber The measure number for this information
     */

    /**
     * @typedef {object} PerformacePackage
     * @property {CleanedPitchesPackage[]} pitches The clean pitches from the performance
     */

    /**
     * Analyzes a performance against the expected performance
     * @param {PerformacePackage} performance The recorded performance
     * @param {ParsedOutputPackage} pData The performance data for all parts
     * @param {number} trackName The name of the track to analyze
     * @param {number} staffIndex The index of the staff to analyze
     * @param {int[]} measureBounds If specified then the start and end of the measure bounds on the staff, if null then no bounds
     * @param {boolean} isDurationExercise if the performance was a duration exercise
     * @return {AnalyzedPerformancePackage[]} An array of analyzed performance objects with information about each measure analyzed
     */
    static analyzePerformance(performance, pData, trackName, staffIndex, measureBounds, isDurationExercise) {
        let trackIndex = null;
        for (let i = 0; i < performance.pitches.length; i++) {
            let note = performance.pitches[i];
            if (i === performance.pitches.length - 1) {
                note['duration'] = 1;
            } else {
                note['duration'] = Math.abs(performance.pitches[i + 1].timepos - note.timepos);
            }
        }
        if (Array.isArray(pData["tracks"])) {
            for (let i = 0; i < pData["tracks"].length; i++) {
                if (pData["tracks"][i].name === trackName) {
                    trackIndex = i;
                    break;
                }
            }
        } else {
            trackIndex = 0;
        }
        let {expectedPerformance, measureSize } = ParserPData.getExpectedPerformance(pData, trackIndex, staffIndex, measureBounds, isDurationExercise);
        const manipulator = new ParsedOutputManipulator(pData);
        let measureLengths = manipulator.getMeasureTimeLength(trackName);

        let measureNumber = 1;
        if (measureBounds !== null) {
            measureNumber = measureBounds[0];
        }
        let measureErrors = [];
        for (let i = 0; i < measureSize; i++) {
            const newMeasureError = {
                durationDifference: 0,
                pitchDifference: 0,
                followBadMeasures: 0,
                overallScore: 0,
                measureNumber: measureNumber
            }
            measureErrors.push(JSON.parse(JSON.stringify(newMeasureError))); //Duration, Pitch, Subsequent Bad, Overall score
            measureNumber++;
        }

        let startIndex = measureBounds !== null ? measureBounds[0] : 0;
        let performedNoteIndex = 0;
        for (let noteIndex = 0; noteIndex < expectedPerformance.length; noteIndex++) {
            if (measureBounds !== null && expectedPerformance[noteIndex]["measureNumber"] > measureBounds[1]) {
                break;
            }
            let expectedMIDI = expectedPerformance[noteIndex]["midiValue"];
            let expectedDuration = expectedPerformance[noteIndex]["duration"];
            let currentMeasureNumber = expectedPerformance[noteIndex]["measureNumber"];
            let performedDuration = 0;

            let comparisonNotes = [];
            let performedMIDI = 0;
            let longestComparisonNote = [-1, 0];

            let shortestExpectedNote = 0.16;
            let wiggleRoom = shortestExpectedNote / 2.0;
            while ((expectedDuration - performedDuration) > wiggleRoom && (performedNoteIndex < performance.pitches.length)) {
                let performedNote = performance.pitches[performedNoteIndex];
                if ((performedDuration + performedNote["duration"]) > (expectedDuration + wiggleRoom)) {
                    comparisonNotes.push([(performedDuration + performedNote["duration"]) - (expectedDuration), performedNote["midival"]]);
                    performedNote["duration"] = (performedDuration + performedNote["duration"]) - (expectedDuration); 
                    performedDuration = expectedDuration;
                } else {
                    performedNoteIndex++;
                    performedDuration += performedNote["duration"];
                    comparisonNotes.push([performedNote["duration"], performedNote["midival"]]);
                }
                if (performedNote["duration"] > longestComparisonNote[1]) {
                    longestComparisonNote = [performedNote["duration"], performedNote["midival"]];
                    performedMIDI = performedNote.midival;
                }
            }
            
            if (performedMIDI === 0) {
                performedMIDI = -1;
            }
            if (performedDuration === 0) {
                performedDuration = -1; //The performance was shorter than expected
            }
            let heldDuration = 0;
            for (let i = 0; i < comparisonNotes.length; i++) {
                let difference = (comparisonNotes[i][1] - expectedMIDI);
                if (((comparisonNotes[i][1] === -1 || expectedMIDI === -1) && (comparisonNotes[i][1] !== expectedMIDI)) || difference > 60) {
                    difference = 60;
                }
                //performedMIDI += Math.abs(difference) * comparisonNotes[i][0];
                if (comparisonNotes[i][1] === longestComparisonNote[1]) {
                    heldDuration += comparisonNotes[i][0];
                }
            }
            let durationDifference = Math.abs((heldDuration - expectedDuration)/(expectedDuration));
            let ignoreDurationDifferencesBelowThisValue = 0;
            if (durationDifference <  ignoreDurationDifferencesBelowThisValue) {
                durationDifference = 0;
            }
            let max = expectedDuration * expectedMIDI;
            let pitchScore = 0;

            let totalMeasureDuration = measureLengths[currentMeasureNumber - 1];

            
            if (max < 0) {
                pitchScore = performedMIDI === -1 ? Math.abs(expectedDuration)/ totalMeasureDuration : 0;
            } else {
                if (performedMIDI === -1 && (performedMIDI !== expectedMIDI)) {
                    pitchScore = 0;
                } else {
                    pitchScore = Math.abs((max - performedMIDI)/max) * Math.abs(expectedDuration)/ totalMeasureDuration;
                }
            }
            //Update the measure errors
            let thisNotesMeasure = expectedPerformance[noteIndex]['measureNumber'] - startIndex;
            measureErrors[thisNotesMeasure]["durationDifference"] += durationDifference;
            measureErrors[thisNotesMeasure]["pitchDifference"] += pitchScore * 100;
            measureErrors[thisNotesMeasure]["overallScore"] += (durationDifference + 1) * pitchScore;
        }

        for (let i = 0; i < measureErrors.length; i++) {
            let currentMeasureError = measureErrors[i];

            if (currentMeasureError["pitchDifference"] < 90) {
                currentMeasureError["overallScore"] = (currentMeasureError["pitchDifference"] / 90) * 50
            } else {
                currentMeasureError["overallScore"] = (currentMeasureError["pitchDifference"] - 90) * 5 + 50;
            }
            for (let j = i; j < measureErrors.length; j++) {
                currentMeasureError["followBadMeasures"] = j - i;
                if (measureErrors[j]["pitchDifference"] <= currentMeasureError["pitchDifference"]) {
                    break;
                }
            }
        }
        return measureErrors;
    }
}
module.exports = PerformanceAnalyser;