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;