281 lines
8.7 KiB
JavaScript
281 lines
8.7 KiB
JavaScript
// @ts-check
|
|
|
|
function setup() {
|
|
createCanvas(1098, 118).parent('piano-visualizer');
|
|
colorMode(HSB, 360, 100, 100, 100);
|
|
smooth(2);
|
|
frameRate(60);
|
|
initKeys();
|
|
|
|
// init config elements
|
|
select('#colorpicker').value(state.color);
|
|
changeColor();
|
|
select('#rainbow-mode-checkbox').checked(state.rainbowMode);
|
|
select('#velocity-mode-checkbox').checked(state.velocityMode);
|
|
}
|
|
|
|
function draw() {
|
|
background(0, 0, 20, 100);
|
|
pushHistories();
|
|
drawWhiteKeys();
|
|
drawBlackKeys();
|
|
drawTexts();
|
|
}
|
|
|
|
function calculateSessionTime() {
|
|
let currentTime = new Date();
|
|
let timeElapsed = +currentTime - +sessionStartTime;
|
|
// Convert time elapsed to hours, minutes, and seconds
|
|
let seconds = Math.floor((timeElapsed / 1000) % 60);
|
|
let minutes = Math.floor((timeElapsed / (1000 * 60)) % 60);
|
|
let hours = Math.floor((timeElapsed / (1000 * 60 * 60)) % 24);
|
|
sessionTotalSeconds = Math.floor(timeElapsed / 1000);
|
|
// Pad minutes and seconds with leading zeros
|
|
let paddedMinutes = String(minutes).padStart(2, '0');
|
|
let paddedSeconds = String(seconds).padStart(2, '0');
|
|
let timeText = `${hours}:${paddedMinutes}:${paddedSeconds}`;
|
|
return timeText;
|
|
}
|
|
|
|
function initKeys() {
|
|
for (let i = 0; i < 128; i++) {
|
|
isKeyOn[i] = 0;
|
|
isPedaled[i] = 0;
|
|
}
|
|
}
|
|
|
|
function drawWhiteKeys() {
|
|
const {velocityMode, rainbowMode} = state;
|
|
let wIndex = 0; // white key index
|
|
stroke(0, 0, 0);
|
|
strokeWeight(1);
|
|
for (let i = 21; i < 109; i++) {
|
|
if (isBlack[i % 12] == 0) {
|
|
// it's a white key
|
|
if (velocityMode) {
|
|
let m = Math.max(isKeyOn[i], isPedaled[i]) * .9 + .1;
|
|
if ((isKeyOn[i] || isPedaled[i]) && !rainbowMode) {
|
|
let whitenedColor = keyOnColor.levels.map(x => Math.floor(x * m + 255 * (1 - m)));
|
|
fill(`rgb(${whitenedColor[0]}, ${whitenedColor[1]}, ${whitenedColor[2]})`); // keypressed
|
|
} else if ((isKeyOn[i] || isPedaled[i]) && rainbowMode) {
|
|
fill(map(i, 21, 108, 0, 1080) % 360, 100 * m, 100, 100); // rainbowMode
|
|
} else {
|
|
fill(0, 0, 100); // white key
|
|
}
|
|
} else {
|
|
if (isKeyOn[i] && !rainbowMode) {
|
|
fill(keyOnColor); // keypressed
|
|
} else if (isKeyOn[i] && rainbowMode) {
|
|
fill(map(i, 21, 108, 0, 1080) % 360, 100, 100, 100); // rainbowMode
|
|
} else if (isPedaled[i] && !rainbowMode) {
|
|
fill(pedaledColor); // pedaled
|
|
} else if (isPedaled[i] && rainbowMode) {
|
|
fill(map(i, 21, 108, 0, 1080) % 360, 100, 70, 100); // pedaled rainbowMode
|
|
} else {
|
|
fill(0, 0, 100); // white key
|
|
}
|
|
}
|
|
let thisX = border + wIndex * (whiteKeyWidth + whiteKeySpace);
|
|
rect(thisX, keyAreaY, whiteKeyWidth, keyAreaHeight, radius);
|
|
// println(wIndex);
|
|
wIndex++;
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawBlackKeys() {
|
|
const {rainbowMode, velocityMode} = state;
|
|
let wIndex = 0; // white key index
|
|
stroke(0, 0, 0);
|
|
strokeWeight(1.5);
|
|
for (let i = 21; i < 109; i++) {
|
|
if (isBlack[i % 12] == 0) {
|
|
// it's a white key
|
|
wIndex++;
|
|
}
|
|
|
|
if (isBlack[i % 12] > 0) {
|
|
// it's a black key
|
|
if (velocityMode) {
|
|
let m = Math.max(isKeyOn[i], isPedaled[i]) * .9 + .1;
|
|
if ((isKeyOn[i] || isPedaled[i]) && !rainbowMode) {
|
|
let whitenedColor = keyOnColor.levels.map(x => Math.floor(x * m + 255 * (1 - m)));
|
|
fill(`rgb(${whitenedColor[0]}, ${whitenedColor[1]}, ${whitenedColor[2]})`); // keypressed
|
|
} else if ((isKeyOn[i] || isPedaled[i]) && rainbowMode) {
|
|
fill(map(i, 21, 108, 0, 1080) % 360, 100 * m, 100, 100); // rainbowMode
|
|
} else {
|
|
fill(0, 0, 0); // black key
|
|
}
|
|
} else {
|
|
if (isKeyOn[i] && !rainbowMode) {
|
|
fill(keyOnColor); // keypressed
|
|
} else if (isKeyOn[i] && rainbowMode) {
|
|
fill(map(i, 21, 108, 0, 1080) % 360, 100, 100, 100); // rainbowMode
|
|
} else if (isPedaled[i] && !rainbowMode) {
|
|
fill(pedaledColor); // pedaled
|
|
} else if (isPedaled[i] && rainbowMode) {
|
|
fill(map(i, 21, 108, 0, 1080) % 360, 100, 70, 100); // pedaled rainbowMode
|
|
} else {
|
|
fill(0, 0, 0); // black key
|
|
}
|
|
}
|
|
|
|
let thisX = border + (wIndex - 1) * (whiteKeyWidth + whiteKeySpace) + isBlack[i % 12];
|
|
rect(thisX, keyAreaY - 1, blackKeyWidth, blackKeyHeight, bRadius);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawTexts() {
|
|
stroke(0, 0, 10, 100);
|
|
fill(0, 0, 100, 90)
|
|
textFont('Monospace');
|
|
textStyle(BOLD);
|
|
textSize(14);
|
|
textAlign(LEFT, TOP);
|
|
|
|
// TIME
|
|
let timeText = 'TIME' + '\n' + calculateSessionTime();
|
|
text(timeText, 5, 79);
|
|
|
|
// PEDAL
|
|
let pedalText = 'PEDALS' + '\nL ' + convertNumberToBars(cc67now) + ' R ' + convertNumberToBars(cc64now)
|
|
text(pedalText, 860, 79);
|
|
|
|
// NOTES
|
|
let notesText = 'NOTE COUNT' + '\n' + totalNotesPlayed;
|
|
text(notesText, 85, 79);
|
|
|
|
// CALORIES
|
|
let caloriesText = 'CALORIES' + '\n' + (totalIntensityScore / 250).toFixed(3); // 250 Intensity = 1 kcal.
|
|
text(caloriesText, 350, 79);
|
|
|
|
// SHORT-TERM DENSITY
|
|
let shortTermDensity = shortTermTotal.reduce((accumulator, currentValue) => accumulator + currentValue, 0); // Sum the array.
|
|
if (shortTermDensity > notesSMax) {
|
|
notesSMax = shortTermDensity
|
|
};
|
|
let shortTermDensityText = 'NPS(MAX)' + '\n' + shortTermDensity + ' (' + notesSMax + ')';
|
|
text(shortTermDensityText, 190, 79);
|
|
|
|
// LEGATO SCORE
|
|
let legatoScore = legatoHistory.reduce((accumulator, currentValue) => accumulator + currentValue, 0)
|
|
legatoScore /= 60;
|
|
let legatoText = 'LEGATO' + '\n' + legatoScore.toFixed(2);
|
|
text(legatoText, 276, 79);
|
|
|
|
// NOW PLAYING
|
|
/** @type {string[]} */
|
|
let chordSymbol = Tonal.Chord.detect(getPressedKeys(), { assumePerfectFifth: true })
|
|
let chordSymbolWithoutM = chordSymbol.map((str) => str.replace(/M($|(?=\/))/g, '')); // get rid of the M's
|
|
let nowPlayingText = truncateString(getPressedKeys().join(' '), 47) + '\n' + truncateString(chordSymbolWithoutM.join(' '), 47);
|
|
text(nowPlayingText, 440, 79);
|
|
}
|
|
|
|
function pushHistories() {
|
|
shortTermTotal.push(notesThisFrame);
|
|
shortTermTotal.shift();
|
|
notesThisFrame = 0;
|
|
legatoHistory.push(isKeyOn.reduce((accumulator, currentValue) => accumulator + !!currentValue, 0));
|
|
legatoHistory.shift();
|
|
}
|
|
|
|
/** @param {number} number */
|
|
function convertNumberToBars(number) {
|
|
if (number < 0 || number > 127) {
|
|
throw new Error('Number must be between 0 and 127');
|
|
}
|
|
|
|
const maxBars = 10;
|
|
const scaleFactor = 128 / maxBars;
|
|
|
|
// Calculate the number of bars
|
|
const numberOfBars = Math.ceil(number / scaleFactor);
|
|
|
|
// Create a string with the calculated number of '|' characters
|
|
const barString = '|'.repeat(numberOfBars);
|
|
|
|
// Calculate the number of '.' characters required to fill the remaining space
|
|
const numberOfDots = maxBars - numberOfBars;
|
|
|
|
// Create a string with the calculated number of '.' characters
|
|
const dotString = '.'.repeat(numberOfDots);
|
|
|
|
// Combine the '|' and '.' strings
|
|
const combinedString = barString + dotString;
|
|
|
|
return combinedString;
|
|
}
|
|
|
|
function getPressedKeys() {
|
|
let pressedOrPedaled = [];
|
|
|
|
for (let i = 0; i < isKeyOn.length; i++) {
|
|
pressedOrPedaled[i] = isKeyOn[i] || isPedaled[i];
|
|
}
|
|
|
|
let noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; // default if sharp
|
|
if (flatNames) {
|
|
// flat
|
|
noteNames = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
|
}
|
|
|
|
const pressedKeys = [];
|
|
|
|
for (let i = 0; i < pressedOrPedaled.length; i++) {
|
|
if (pressedOrPedaled[i]) {
|
|
const noteName = noteNames[i % 12];
|
|
const octave = Math.floor(i / 12) - 1;
|
|
pressedKeys.push(`${noteName}${octave}`);
|
|
}
|
|
}
|
|
return pressedKeys;
|
|
}
|
|
|
|
/** @param {string} str */
|
|
function truncateString(str, maxLength = 40) {
|
|
if (str.length <= maxLength) {
|
|
return str;
|
|
}
|
|
return str.slice(0, maxLength - 3) + '...';
|
|
}
|
|
|
|
function mouseClicked() {
|
|
// Save the canvas content as an image file
|
|
if (mouseX < 50 && mouseY < 50) {
|
|
const now = new Date();
|
|
const strDate =
|
|
now.getFullYear() +
|
|
String(now.getMonth()+1).padStart(2, '0') +
|
|
String(now.getDate()).padStart(2, '0');
|
|
const strTime =
|
|
String(now.getHours()).padStart(2, '0') +
|
|
String(now.getMinutes()).padStart(2, '0') +
|
|
String(now.getSeconds()).padStart(2, '0');
|
|
const fileName = `nicechord-pianometer-${strDate}_${strTime}`;
|
|
saveCanvas(fileName, 'png');
|
|
}
|
|
|
|
if (mouseY > 76) {
|
|
if (mouseX <= 84) {
|
|
sessionStartTime = new Date();
|
|
}
|
|
|
|
if (mouseX > 84 && mouseX < 170) {
|
|
totalNotesPlayed = 0;
|
|
}
|
|
|
|
if (mouseX > 187 && mouseX < 257) {
|
|
notesSMax = 0;
|
|
}
|
|
|
|
if (mouseX > 347 && mouseX < 420) {
|
|
totalIntensityScore = 0; // RESET CALORIES
|
|
}
|
|
|
|
if (mouseX > 441 && mouseX < 841) {
|
|
flatNames = !flatNames; // toggle flat
|
|
}
|
|
}
|
|
console.log(mouseX, mouseY);
|
|
}
|