Add files via upload

First upload.
This commit is contained in:
Wiwi Kuan 2023-03-29 01:16:08 +08:00 committed by GitHub
parent 6e81aec6e8
commit b458744d00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 5366 additions and 0 deletions

122
globals.js Normal file
View file

@ -0,0 +1,122 @@
let midiSelectSlider;
// for piano visualizer
let nowPedaling = false; // is it pedaling?(不要動)
let isKeyOn = []; // what notes are being pressed (1 or 0)(不要動)
let isPedaled = []; // what notes are pedaled (1 or 0)(不要動)
let keyOnColor // set it in setup()
let pedaledColor // set it in setup()
let isBlack = [0, 11, 0, 13, 0, 0, 11, 0, 12, 0, 13, 0]; // 是黑鍵嗎?是的話,相對左方的白鍵位移多少?(default: {0, 11, 0, 13, 0, 0, 11, 0, 12, 0, 13, 0}
let border = 3; // 左方留空幾個畫素?(default: 3)
let whiteKeyWidth = 20; // 白鍵多寬?(default: 20)
let whiteKeySpace = 1; // 白鍵間的縫隙多寬?(default: 1)
let blackKeyWidth = 17; // 黑鍵多寬?(default: 17)
let blackKeyHeight = 45; // 黑鍵多高?(default: 45)
let radius = 5; // 白鍵圓角(default: 5)
let bRadius = 4; // 黑鍵圓角(default: 4)
let keyAreaY = 3; // 白鍵從 Y 軸座標多少開始?(default: 3)
let keyAreaHeight = 70; // 白鍵多高?(default: 70)
let rainbowMode = false; // 彩虹模式 (default: false)
let cc64now = 0; // 現在的踏板狀態
let cc67now = 0;
let sessionStartTime = new Date();
let sessionTotalSeconds = 0;
// note counter
let notesThisFrame = 0;
let totalNotesPlayed = 0;
let shortTermTotal = new Array(60).fill(0);
let legatoHistory = new Array(60).fill(0);
let totalIntensityScore = 0;
// for key pressed counter
let notePressedCount = 0;
let notePressedCountHistory = [];
WebMidi.enable(function (err) { //check if WebMidi.js is enabled
if (err) {
console.log("WebMidi could not be enabled.", err);
} else {
console.log("WebMidi enabled!");
}
//name our visible MIDI input and output ports
console.log("---");
console.log("Inputs Ports: ");
for (i = 0; i < WebMidi.inputs.length; i++) {
console.log(i + ": " + WebMidi.inputs[i].name);
}
console.log("---");
console.log("Output Ports: ");
for (i = 0; i < WebMidi.outputs.length; i++) {
console.log(i + ": " + WebMidi.outputs[i].name);
}
midiSelectSlider = select("#slider");
midiSelectSlider.attribute("max", WebMidi.inputs.length - 1);
midiSelectSlider.changed(inputChanged);
midiIn = WebMidi.inputs[midiSelectSlider.value()]
inputChanged();
});
function inputChanged() {
midiIn.removeListener();
midiIn = WebMidi.inputs[midiSelectSlider.value()];
midiIn.addListener('noteon', "all", function (e) {
console.log("Received 'noteon' message (" + e.note.number + ", " + e.velocity + ").");
noteOn(e.note.number, e.velocity);
});
midiIn.addListener('noteoff', "all", function (e) {
console.log("Received 'noteoff' message (" + e.note.number + ", " + e.velocity + ").");
noteOff(e.note.number, e.velocity);
})
midiIn.addListener('controlchange', 'all', function(e) {
console.log("Received control change message:", e.controller.number, e.value);
controllerChange(e.controller.number, e.value)
});
console.log(midiIn.name);
select("#device").html(midiIn.name);
};
function noteOn(pitch, velocity) {
totalNotesPlayed++;
notesThisFrame++;
totalIntensityScore += velocity;
// piano visualizer
isKeyOn[pitch] = 1;
if (nowPedaling) {
isPedaled[pitch] = 1;
}
}
function noteOff(pitch, velocity) {
isKeyOn[pitch] = 0;
}
function controllerChange(number, value) {
// Receive a controllerChange
if (number == 64) {
cc64now = value;
if (value >= 64) {
nowPedaling = true;
for (let i = 0; i < 128; i++) {
// copy key on to pedal
isPedaled[i] = isKeyOn[i];
}
} else if (value < 64) {
nowPedaling = false;
for (let i = 0; i < 128; i++) {
// reset isPedaled
isPedaled[i] = 0;
}
}
}
if (number == 67) {
cc67now = value;
}
}

46
index.html Normal file
View file

@ -0,0 +1,46 @@
<html>
<head>
<meta charset="UTF-8">
<title>好和弦的鋼琴鍵盤顯示器</title>
<link rel="stylesheet" href="style.css">
<script src="p5.min.js"></script>
<script src="webmidi.js"></script>
<script src="globals.js"></script>
<script src="piano-visualizer.js"></script>
</head>
<body>
<div id="main">
<div id="controls" class="center">
<div>
<h3>鋼琴鍵盤顯示器 by NiceChord</h3>
<h5>選擇 MIDI 裝置</h5>
<input id="slider" type="range" min="0" max="0" value="0">
<div id="device">Select Input: </div>
</div>
<br />
</div>
</div>
<div class="center">
<div id="piano-visualizer">
<!-- Our sketch will go here! -->
</div>
<span style="font-size: 11px;">
TIME使用時間 | NOTE COUNT總彈奏音符數 | NOTES/S最近一秒鐘彈奏音符數 | LEGATO圓滑指數最近一秒鐘平均來說有幾個鍵被同時按住 <br />
CALORIES消耗熱量估計值好玩就好| KEYS現在正在被按住或被踏板留住的音 | PEDALS左右踏板深度顯示 <br />
(密技:點鍵盤最左上角的角落,可以儲存截圖) <br /><br />
</span>
覺得好用嗎?到 <a href="https://nicechord.com">NiceChord.com</a> 逛逛支持我!
</div>
</div>
</body>
</html>

2
p5.min.js vendored Normal file

File diff suppressed because one or more lines are too long

222
piano-visualizer.js Normal file
View file

@ -0,0 +1,222 @@
function setup() {
createCanvas(1098, 118).parent('piano-visualizer');
colorMode(HSB, 360, 100, 100, 100);
keyOnColor = color(326, 100, 100, 100); // <---- 編輯這裡換「按下時」的顏色![HSB Color Mode]
pedaledColor = color(326, 100, 70, 100); // <---- 編輯這裡換「踏板踩住」的顏色![HSB Color Mode]
smooth(2);
frameRate(60);
initKeys();
}
function draw() {
background(0, 0, 20, 100);
pushHistories();
drawWhiteKeys();
drawBlackKeys();
// drawPedalLines();
// drawNotes();
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 (i = 0; i<128; i++) {
isKeyOn[i] = 0;
isPedaled[i] = 0;
}
}
function drawWhiteKeys() {
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 (isKeyOn[i] == 1 && !rainbowMode) {
fill(keyOnColor); // keypressed
} else if (isKeyOn[i] == 1 && rainbowMode) {
fill(map(i, 21, 108, 0, 1080)%360, 100, 100, 100); // rainbowMode
} else if (isPedaled[i] == 1 && !rainbowMode) {
fill(pedaledColor); // pedaled
} else if (isPedaled[i] == 1 && 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() {
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 (isKeyOn[i] == 1 && !rainbowMode) {
fill(keyOnColor); // keypressed
} else if (isKeyOn[i] == 1 && rainbowMode) {
fill(map(i, 21, 108, 0, 1080)%360, 100, 100, 100); // rainbowMode
} else if (isPedaled[i] == 1 && !rainbowMode) {
fill(pedaledColor); // pedaled
} else if (isPedaled[i] == 1 && rainbowMode) {
fill(map(i, 21, 108, 0, 1080)%360, 100, 70, 100); // pedaled rainbowMode
} else {
fill(0, 0, 0); // white 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, 95, 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.
let shortTermDensityText = "NOTES/S" + "\n" + shortTermDensity;
text(shortTermDensityText, 200, 79);
// LEGATO SCORE
let legatoScore = legatoHistory.reduce((accumulator, currentValue) => accumulator + currentValue, 0)
legatoScore /= 60;
let legatoText = "LEGATO" + "\n" + legatoScore.toFixed(2);
text(legatoText, 280, 79);
// NOW PLAYING
let nowPlayingText = "KEYS" + "\n" + truncateString(getPressedKeys(), 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();
}
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] === 1 || isPedaled[i] === 1 ? 1 : 0;
}
let noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; // default if sharp
if ([0, 1, 3, 5, 8, 10].includes(pressedOrPedaled.indexOf(1) % 12)) {
// 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] === 1) {
const noteName = noteNames[i % 12];
const octave = Math.floor(i / 12) - 1;
pressedKeys.push(`${noteName}${octave}`);
}
}
return pressedKeys.join(' ');
}
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) {
saveCanvas('nicechord-pianometer', 'png');
}
}

547
style.css Normal file
View file

@ -0,0 +1,547 @@
/*! style.css v1.0.0 | ISC License | https://github.com/ungoldman/style.css */
html {
color: rgb(210, 209, 202);
background-color: #202328;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "avenir next", avenir, "segoe ui", "fira sans", roboto, noto, "droid sans", "liberation sans", "lucida grande", "helvetica neue", helvetica, "franklin gothic medium", "century gothic", cantarell, oxygen, ubuntu, sans-serif;
font-size: calc(14px + 0.25vw);
line-height: 1.55;
-webkit-font-kerning: normal;
font-kerning: normal;
text-rendering: optimizeLegibility;
-webkit-font-feature-settings: "kern", "liga"1, "calt"0;
font-feature-settings: "kern", "liga"1, "calt"0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
height: 100%;
}
#main {
display: flex;
margin-top: 0.5rem;
flex-wrap: wrap;
justify-content: space-evenly;
align-items: center;
}
#controls {
width: 600px;
height: 100%;
}
#piano-visualizer {
margin-top: 0.2rem;
margin-bottom: 1rem;
}
.center {
text-align: center;
}
.left {
text-align: left;
}
body {
margin: 0;
height: 100%;
}
article,
aside,
footer,
header,
nav,
section,
figcaption,
figure,
main {
display: block;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
p,
blockquote,
ul,
ol,
dl,
table,
pre {
margin-top: 0;
margin-bottom: 1.25em;
}
small {
font-size: 80%;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 500;
line-height: 1.25em;
margin-top: 0;
margin-bottom: 0.5rem;
position: relative;
text-align: center;
color: #ddd;
}
h1 small,
h2 small,
h3 small,
h4 small,
h5 small,
h6 small {
color: #777;
font-size: 0.7em;
font-weight: 300;
}
h1 code,
h2 code,
h3 code,
h4 code,
h5 code,
h6 code {
font-size: 0.9em;
}
h1 {
font-size: 2.75em;
}
h2 {
font-size: 2.25em;
}
h3 {
font-size: 1.75em;
}
h4 {
font-size: 1.5em;
}
h5 {
font-size: 1.25em;
}
h6 {
font-size: 1.15em;
color: #575757;
}
p {
letter-spacing: -0.01em;
}
a {
background-color: transparent;
-webkit-text-decoration-skip: objects;
color: #0074d9;
text-decoration: none;
}
a:active,
a:hover {
outline-width: 0;
outline: 0;
}
a:active,
a:focus,
a:hover {
text-decoration: underline;
}
ul,
ol {
padding: 0;
padding-left: 2em;
}
ul ol,
ol ol {
list-style-type: lower-roman;
}
ul ul,
ul ol,
ol ul,
ol ol {
margin-top: 0;
margin-bottom: 0;
}
ul ul ol,
ul ol ol,
ol ul ol,
ol ol ol {
list-style-type: lower-alpha;
}
li>p {
margin-top: 1em;
}
blockquote {
margin: 0 0 1rem;
padding: 0 1rem;
color: #7d7d7d;
border-left: 4px solid #d6d6d6;
}
blockquote> :first-child {
margin-top: 0;
}
blockquote> :last-child {
margin-bottom: 0;
}
b,
strong {
font-weight: inherit;
font-weight: 600;
}
mark {
background-color: #ff0;
color: #000;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
code,
pre,
kbd,
samp {
font-family: menlo, inconsolata, consolas, "fira mono", "noto mono", "droid sans mono", "liberation mono", "dejavu sans mono", "ubuntu mono", monaco, "courier new", monospace;
font-size: 90%;
}
pre,
code {
background-color: #f7f7f7;
border-radius: 3px;
}
pre {
overflow: auto;
word-wrap: normal;
padding: 1em;
line-height: 1.45;
}
pre code {
background: transparent;
display: inline;
padding: 0;
line-height: inherit;
word-wrap: normal;
}
pre code::before,
pre code::after {
content: normal;
}
pre>code {
border: 0;
font-size: 1em;
white-space: pre;
word-break: normal;
}
code {
padding: 0.2em 0;
margin: 0;
}
code::before,
code::after {
letter-spacing: -0.2em;
content: '\00a0';
}
kbd {
background-color: #e6e6e6;
background-image: linear-gradient(#fafafa, #e6e6e6);
background-repeat: repeat-x;
border: 1px solid #d6d6d6;
border-radius: 2px;
box-shadow: 0 1px 0 #d6d6d6;
color: #303030;
display: inline-block;
line-height: 0.95em;
margin: 0 1px;
padding: 5px 5px 1px;
}
td,
th {
padding: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all;
}
table th,
table td {
padding: 6px 13px;
border: 1px solid #ddd;
}
table th {
font-weight: bold;
}
table tr {
background-color: #fff;
border-top: 1px solid #ccc;
}
table tr:nth-child(2n) {
background-color: #f8f8f8;
}
hr {
box-sizing: content-box;
overflow: visible;
background: transparent;
height: 4px;
padding: 0;
margin: 1em 0;
background-color: #e7e7e7;
border: 0 none;
}
hr::before {
display: table;
content: '';
}
hr::after {
display: table;
clear: both;
content: '';
}
img {
border-style: none;
border: 0;
max-width: 100%;
}
svg:not(:root) {
overflow: hidden;
}
figure {
margin: 1em 0;
}
figure img {
background: white;
border: 1px solid #c7c7c7;
padding: 0.25em;
}
figcaption {
font-style: italic;
font-size: 0.75em;
font-weight: 200;
margin: 0;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
dfn {
font-style: italic;
}
dd {
margin-left: 0;
}
dl {
padding: 0;
}
dl dt {
padding: 0;
margin-top: 1em;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
dl dd {
padding: 0 1em;
margin-bottom: 1.25em;
}
audio,
video {
display: inline-block;
}
audio:not([controls]) {
display: none;
height: 0;
}
input {
margin: 0;
}
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
progress {
display: inline-block;
vertical-align: baseline;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
[disabled] {
cursor: default;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
details,
menu {
display: block;
}
summary {
display: list-item;
}
canvas {
display: inline-block;
}
template {
display: none;
}
[hidden] {
display: none;
}

4427
webmidi.js Normal file

File diff suppressed because it is too large Load diff