Compare commits
5 commits
3377d1cf35
...
c67263e4ea
Author | SHA1 | Date | |
---|---|---|---|
c67263e4ea | |||
a6887a8046 | |||
94646d36fc | |||
|
165fb6134c | ||
|
810bae60b8 |
7 changed files with 345 additions and 200 deletions
|
@ -1,4 +1,4 @@
|
||||||
# 鋼琴鍵盤顯示器 by 好和弦
|
# 鋼琴鍵盤顯示器 by 好和弦+サポミク
|
||||||
|
|
||||||
88 鍵鋼琴鍵盤顯示器,練琴、拍影片、直播的好幫手!有以下的功能:
|
88 鍵鋼琴鍵盤顯示器,練琴、拍影片、直播的好幫手!有以下的功能:
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
現在就用用看:https://nicechord.com/pianometer/
|
現在就用用看:https://nicechord.com/pianometer/
|
||||||
|
|
||||||
# PianoMeter by NiceChord
|
# PianoMeter by NiceChord+sup39
|
||||||
|
|
||||||
An 88-key piano keyboard display, great for practicing, recording videos, and live streaming! It has the following features:
|
An 88-key piano keyboard display, great for practicing, recording videos, and live streaming! It has the following features:
|
||||||
|
|
||||||
|
@ -34,4 +34,4 @@ An 88-key piano keyboard display, great for practicing, recording videos, and li
|
||||||
|
|
||||||
Introduction video (with English subtitles!): [PeerTube](https://wiwi.video/w/uzo8aZmdx1rvYJfWZBgmCM) | [YouTube](https://www.youtube.com/watch?v=YsL7WGUEU-4)
|
Introduction video (with English subtitles!): [PeerTube](https://wiwi.video/w/uzo8aZmdx1rvYJfWZBgmCM) | [YouTube](https://www.youtube.com/watch?v=YsL7WGUEU-4)
|
||||||
|
|
||||||
Try it now: https://nicechord.com/pianometer/
|
Try it now: https://piano.miku.sup39.dev/pianometer/
|
||||||
|
|
99
globals.js
99
globals.js
|
@ -1,11 +1,21 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
let midiSelectSlider;
|
let midiSelectSlider;
|
||||||
|
let midiIn;
|
||||||
|
|
||||||
// for piano visualizer
|
// for piano visualizer
|
||||||
|
const state = State('pianometer/', {
|
||||||
|
color: '#2ee5b8',
|
||||||
|
rainbowMode: false, // 彩虹模式
|
||||||
|
velocityMode: false, // 力度模式
|
||||||
|
});
|
||||||
|
/** @type {Color} */
|
||||||
|
let keyOnColor; // 「按下時」的顏色 [HSB Color Mode]
|
||||||
|
/** @type {Color} */
|
||||||
|
let pedaledColor; // 「踏板踩住」的顏色 [HSB Color Mode]
|
||||||
let nowPedaling = false; // is it pedaling?(不要動)
|
let nowPedaling = false; // is it pedaling?(不要動)
|
||||||
let isKeyOn = []; // what notes are being pressed (1 or 0)(不要動)
|
let isKeyOn = []; // what notes are being pressed (1 or 0)(不要動)
|
||||||
let isPedaled = []; // what notes are pedaled (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 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 border = 3; // 左方留空幾個畫素?(default: 3)
|
||||||
let whiteKeyWidth = 20; // 白鍵多寬?(default: 20)
|
let whiteKeyWidth = 20; // 白鍵多寬?(default: 20)
|
||||||
|
@ -16,7 +26,6 @@ let radius = 5; // 白鍵圓角(default: 5)
|
||||||
let bRadius = 4; // 黑鍵圓角(default: 4)
|
let bRadius = 4; // 黑鍵圓角(default: 4)
|
||||||
let keyAreaY = 3; // 白鍵從 Y 軸座標多少開始?(default: 3)
|
let keyAreaY = 3; // 白鍵從 Y 軸座標多少開始?(default: 3)
|
||||||
let keyAreaHeight = 70; // 白鍵多高?(default: 70)
|
let keyAreaHeight = 70; // 白鍵多高?(default: 70)
|
||||||
let rainbowMode = false; // 彩虹模式 (default: false)
|
|
||||||
let cc64now = 0; // 現在的踏板狀態
|
let cc64now = 0; // 現在的踏板狀態
|
||||||
let cc67now = 0;
|
let cc67now = 0;
|
||||||
|
|
||||||
|
@ -39,25 +48,26 @@ let notePressedCountHistory = [];
|
||||||
|
|
||||||
WebMidi.enable(function (err) { // check if WebMidi.js is enabled
|
WebMidi.enable(function (err) { // check if WebMidi.js is enabled
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log("WebMidi could not be enabled.", err);
|
console.log('WebMidi could not be enabled.', err);
|
||||||
} else {
|
} else {
|
||||||
console.log("WebMidi enabled!");
|
console.log('WebMidi enabled!');
|
||||||
}
|
}
|
||||||
|
|
||||||
//name our visible MIDI input and output ports
|
//name our visible MIDI input and output ports
|
||||||
console.log("---");
|
console.log('---');
|
||||||
console.log("Inputs Ports: ");
|
console.log('Inputs Ports: ');
|
||||||
for (i = 0; i < WebMidi.inputs.length; i++) {
|
WebMidi.inputs.forEach(({name}, i) => {
|
||||||
console.log(i + ": " + WebMidi.inputs[i].name);
|
console.log(i + ': ' + name);
|
||||||
}
|
});
|
||||||
|
|
||||||
console.log("---");
|
console.log('---');
|
||||||
console.log("Output Ports: ");
|
console.log('Output Ports: ');
|
||||||
for (i = 0; i < WebMidi.outputs.length; i++) {
|
WebMidi.outputs.forEach(({name}, i) => {
|
||||||
console.log(i + ": " + WebMidi.outputs[i].name);
|
console.log(i + ': ' + name);
|
||||||
}
|
});
|
||||||
midiSelectSlider = select("#slider");
|
|
||||||
midiSelectSlider.attribute("max", WebMidi.inputs.length - 1);
|
midiSelectSlider = select('#slider');
|
||||||
|
midiSelectSlider.attribute('max', WebMidi.inputs.length - 1);
|
||||||
midiSelectSlider.input(inputChanged);
|
midiSelectSlider.input(inputChanged);
|
||||||
midiIn = WebMidi.inputs[midiSelectSlider.value()]
|
midiIn = WebMidi.inputs[midiSelectSlider.value()]
|
||||||
inputChanged();
|
inputChanged();
|
||||||
|
@ -70,38 +80,50 @@ function inputChanged() {
|
||||||
|
|
||||||
midiIn.removeListener();
|
midiIn.removeListener();
|
||||||
midiIn = WebMidi.inputs[midiSelectSlider.value()];
|
midiIn = WebMidi.inputs[midiSelectSlider.value()];
|
||||||
midiIn.addListener('noteon', "all", function (e) {
|
midiIn.addListener('noteon', 'all', function (e) {
|
||||||
console.log("Received 'noteon' message (" + e.note.number + ", " + e.velocity + ").");
|
console.log('Received \'noteon\' message (' + e.note.number + ', ' + e.velocity + ').');
|
||||||
noteOn(e.note.number, e.velocity);
|
noteOn(e.note.number, e.velocity);
|
||||||
});
|
});
|
||||||
midiIn.addListener('noteoff', "all", function (e) {
|
midiIn.addListener('noteoff', 'all', function (e) {
|
||||||
console.log("Received 'noteoff' message (" + e.note.number + ", " + e.velocity + ").");
|
console.log('Received \'noteoff\' message (' + e.note.number + ', ' + e.velocity + ').');
|
||||||
noteOff(e.note.number, e.velocity);
|
noteOff(e.note.number, e.velocity);
|
||||||
})
|
})
|
||||||
midiIn.addListener('controlchange', 'all', function(e) {
|
midiIn.addListener('controlchange', 'all', function(e) {
|
||||||
console.log("Received control change message:", e.controller.number, e.value);
|
console.log('Received control change message:', e.controller.number, e.value);
|
||||||
controllerChange(e.controller.number, e.value)
|
controllerChange(e.controller.number, e.value)
|
||||||
});
|
});
|
||||||
console.log(midiIn.name);
|
console.log(midiIn.name);
|
||||||
select("#device").html(midiIn.name);
|
select('#device').html(midiIn.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} pitch
|
||||||
|
* @param {number} velocity
|
||||||
|
*/
|
||||||
function noteOn(pitch, velocity) {
|
function noteOn(pitch, velocity) {
|
||||||
totalNotesPlayed++;
|
totalNotesPlayed++;
|
||||||
notesThisFrame++;
|
notesThisFrame++;
|
||||||
totalIntensityScore += velocity;
|
totalIntensityScore += velocity;
|
||||||
|
|
||||||
// piano visualizer
|
// piano visualizer
|
||||||
isKeyOn[pitch] = 1;
|
isKeyOn[pitch] = velocity;
|
||||||
if (nowPedaling) {
|
if (nowPedaling) {
|
||||||
isPedaled[pitch] = 1;
|
isPedaled[pitch] = velocity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteOff(pitch, velocity) {
|
/**
|
||||||
|
* @param {number} pitch
|
||||||
|
* @param {number} _velocity
|
||||||
|
*/
|
||||||
|
function noteOff(pitch, _velocity) {
|
||||||
isKeyOn[pitch] = 0;
|
isKeyOn[pitch] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} number
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
function controllerChange(number, value) {
|
function controllerChange(number, value) {
|
||||||
// Receive a controllerChange
|
// Receive a controllerChange
|
||||||
if (number == 64) {
|
if (number == 64) {
|
||||||
|
@ -124,21 +146,30 @@ function controllerChange(number, value) {
|
||||||
|
|
||||||
if (number == 67) {
|
if (number == 67) {
|
||||||
cc67now = value;
|
cc67now = value;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLInputElement} cb
|
||||||
|
*/
|
||||||
function toggleRainbowMode(cb) {
|
function toggleRainbowMode(cb) {
|
||||||
rainbowMode = cb.checked;
|
state.rainbowMode = cb.checked;
|
||||||
if (rainbowMode)
|
if (state.rainbowMode) {
|
||||||
select('#colorpicker').attribute('disabled', true)
|
select('#colorpicker').attribute('disabled', true)
|
||||||
else
|
} else {
|
||||||
select('#colorpicker').removeAttribute('disabled')
|
select('#colorpicker').removeAttribute('disabled')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLInputElement} cb
|
||||||
|
*/
|
||||||
|
function toggleVelocityMode(cb) {
|
||||||
|
state.velocityMode = cb.checked;
|
||||||
|
}
|
||||||
|
|
||||||
function changeColor() {
|
function changeColor() {
|
||||||
keyOnColor = pedaledColor = color(select('#colorpicker').value());
|
keyOnColor = color(state.color = select('#colorpicker').value());
|
||||||
darkenedColor = keyOnColor.levels.map(x => floor(x * .7));
|
const darkenedColor = keyOnColor.levels.map(x => Math.floor(x * .7));
|
||||||
pedaledColor = color(`rgb(${darkenedColor[0]}, ${darkenedColor[1]}, ${darkenedColor[2]})`)
|
pedaledColor = color(`rgb(${darkenedColor[0]}, ${darkenedColor[1]}, ${darkenedColor[2]})`);
|
||||||
console.log(pedaledColor.levels);
|
|
||||||
}
|
}
|
49
index.d.ts
vendored
Normal file
49
index.d.ts
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
const WebMidi: {
|
||||||
|
enable(callback: (err: Error|null)=>void): void
|
||||||
|
inputs: MidiIn[]
|
||||||
|
outputs: any[]
|
||||||
|
};
|
||||||
|
type MidiIn = {
|
||||||
|
name: string
|
||||||
|
addListener(type: 'noteon', channel: string, listener: (e: {
|
||||||
|
note: {number: number}
|
||||||
|
velocity: number
|
||||||
|
})=>void): void
|
||||||
|
addListener(type: 'noteoff', channel: string, listener: (e: {
|
||||||
|
note: {number: number}
|
||||||
|
velocity: number
|
||||||
|
})=>void): void
|
||||||
|
addListener(type: 'controlchange', channel: string, listener: (e: {
|
||||||
|
controller: {number: number}
|
||||||
|
value: number
|
||||||
|
})=>void): void
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tonal: any;
|
||||||
|
|
||||||
|
function select(selector: string): any;
|
||||||
|
|
||||||
|
function color(color: string): Color;
|
||||||
|
type Color = {
|
||||||
|
levels: number[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCanvas: Function;
|
||||||
|
const colorMode: Function;
|
||||||
|
const smooth: Function;
|
||||||
|
const frameRate: Function;
|
||||||
|
const {HSB, BOLD, LEFT, TOP}: any;
|
||||||
|
const background: Function;
|
||||||
|
const fill: Function;
|
||||||
|
const stroke: Function;
|
||||||
|
const strokeWeight: Function;
|
||||||
|
const rect: Function;
|
||||||
|
const textFont: Function;
|
||||||
|
const textStyle: Function;
|
||||||
|
const textSize: Function;
|
||||||
|
const textAlign: Function;
|
||||||
|
const text: Function;
|
||||||
|
const saveCanvas: Function;
|
||||||
|
const {mouseX, mouseY}: number;
|
||||||
|
|
||||||
|
function map(...argv: any[]): number;
|
23
index.html
23
index.html
|
@ -1,3 +1,4 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
@ -8,6 +9,7 @@
|
||||||
<script src="tonal.min.js"></script>
|
<script src="tonal.min.js"></script>
|
||||||
|
|
||||||
<script src="webmidi.js"></script>
|
<script src="webmidi.js"></script>
|
||||||
|
<script src="lib.js"></script>
|
||||||
<script src="globals.js"></script>
|
<script src="globals.js"></script>
|
||||||
<script src="piano-visualizer.js"></script>
|
<script src="piano-visualizer.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -16,7 +18,7 @@
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div id="controls" class="center">
|
<div id="controls" class="center">
|
||||||
<div>
|
<div>
|
||||||
<h3>鋼琴鍵盤顯示器 by NiceChord</h3>
|
<h3>鋼琴鍵盤顯示器 by NiceChord+sup39</h3>
|
||||||
<div style="display: flex; justify-content: space-around;">
|
<div style="display: flex; justify-content: space-around;">
|
||||||
<div>
|
<div>
|
||||||
<h5>選擇 MIDI 裝置</h5>
|
<h5>選擇 MIDI 裝置</h5>
|
||||||
|
@ -31,7 +33,14 @@
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<span style="margin-right: 5px;">彩虹模式</span>
|
<span style="margin-right: 5px;">彩虹模式</span>
|
||||||
<input type="checkbox" id="rainbow-mode-checkbox" onclick="toggleRainbowMode(this)">
|
<input type="checkbox" id="rainbow-mode-checkbox" onclick="toggleRainbowMode(this)">
|
||||||
<label for="rainbow-mode-checkbox">
|
<label for="rainbow-mode-checkbox" class="custom-checkbox">
|
||||||
|
<span class="switch-txt" turnOn="On" turnOff="Off"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span style="margin-right: 5px;">力度模式</span>
|
||||||
|
<input type="checkbox" id="velocity-mode-checkbox" onclick="toggleVelocityMode(this)">
|
||||||
|
<label for="velocity-mode-checkbox" class="custom-checkbox">
|
||||||
<span class="switch-txt" turnOn="On" turnOff="Off"></span>
|
<span class="switch-txt" turnOn="On" turnOff="Off"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,12 +48,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="center">
|
<div class="center">
|
||||||
|
|
||||||
<div id="piano-visualizer">
|
<div id="piano-visualizer">
|
||||||
<!-- Our sketch will go here! -->
|
<!-- Our sketch will go here! -->
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,12 +60,10 @@
|
||||||
CALORIES:消耗熱量(估計值,好玩就好)| PEDALS:左右踏板深度顯示 <br />
|
CALORIES:消耗熱量(估計值,好玩就好)| PEDALS:左右踏板深度顯示 <br />
|
||||||
(密技:點鍵盤最左上角的角落,可以儲存截圖) <br /><br />
|
(密技:點鍵盤最左上角的角落,可以儲存截圖) <br /><br />
|
||||||
</span>
|
</span>
|
||||||
|
<div>
|
||||||
|
<div>覺得好用嗎?到 <a href="https://nicechord.com">NiceChord.com</a> 逛逛支持好和弦!好和弦原版原始碼在 <a href="https://github.com/wiwikuan/pianometer">GitHub</a>。</div>
|
||||||
覺得好用嗎?到 <a href="https://nicechord.com">NiceChord.com</a> 逛逛支持我!原始碼在 <a href="https://github.com/wiwikuan/pianometer">GitHub</a>。
|
<div>此版本經 <a href="https://post.sup39.dev/@miku">サポミク</a> 加工過。加工後的原始碼在 <a href="https://forgejo.sup39.dev/sup39/pianometer">supGit</a>。</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
37
lib.js
Normal file
37
lib.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* @template {Record<string, any>} T
|
||||||
|
* @param {string} keyPrefix
|
||||||
|
* @param {T} defaultState
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
function State(keyPrefix, defaultState) {
|
||||||
|
/** @param {string} key */
|
||||||
|
function getFromRegistry(key) {
|
||||||
|
try {
|
||||||
|
// TODO validate type
|
||||||
|
const rawValue = localStorage.getItem(keyPrefix+key);
|
||||||
|
if (rawValue == null) return null;
|
||||||
|
return JSON.parse(rawValue);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Proxy(/**@type T*/(Object.fromEntries(
|
||||||
|
Object.entries(defaultState)
|
||||||
|
.map(([k, v]) => [k, getFromRegistry(k) ?? v]),
|
||||||
|
)), {
|
||||||
|
get(cache, key) {
|
||||||
|
if (typeof key === 'symbol') return undefined;
|
||||||
|
// cache hit
|
||||||
|
if (key in cache) return cache[key];
|
||||||
|
// cache miss => get from localStorage
|
||||||
|
return /**@type{any}*/(cache)[key] = getFromRegistry(key);
|
||||||
|
},
|
||||||
|
set(cache, key, value) {
|
||||||
|
if (typeof key === 'symbol') return false;
|
||||||
|
/**@type{any}*/(cache)[key] = value;
|
||||||
|
localStorage.setItem(keyPrefix+key, JSON.stringify(value));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,12 +1,17 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
createCanvas(1098, 118).parent('piano-visualizer');
|
createCanvas(1098, 118).parent('piano-visualizer');
|
||||||
colorMode(HSB, 360, 100, 100, 100);
|
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);
|
smooth(2);
|
||||||
frameRate(60);
|
frameRate(60);
|
||||||
initKeys();
|
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() {
|
function draw() {
|
||||||
|
@ -14,15 +19,12 @@ function draw() {
|
||||||
pushHistories();
|
pushHistories();
|
||||||
drawWhiteKeys();
|
drawWhiteKeys();
|
||||||
drawBlackKeys();
|
drawBlackKeys();
|
||||||
// drawPedalLines();
|
|
||||||
// drawNotes();
|
|
||||||
|
|
||||||
drawTexts();
|
drawTexts();
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateSessionTime() {
|
function calculateSessionTime() {
|
||||||
let currentTime = new Date();
|
let currentTime = new Date();
|
||||||
let timeElapsed = currentTime - sessionStartTime;
|
let timeElapsed = +currentTime - +sessionStartTime;
|
||||||
// Convert time elapsed to hours, minutes, and seconds
|
// Convert time elapsed to hours, minutes, and seconds
|
||||||
let seconds = Math.floor((timeElapsed / 1000) % 60);
|
let seconds = Math.floor((timeElapsed / 1000) % 60);
|
||||||
let minutes = Math.floor((timeElapsed / (1000 * 60)) % 60);
|
let minutes = Math.floor((timeElapsed / (1000 * 60)) % 60);
|
||||||
|
@ -36,30 +38,43 @@ function calculateSessionTime() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initKeys() {
|
function initKeys() {
|
||||||
for (i = 0; i < 128; i++) {
|
for (let i = 0; i < 128; i++) {
|
||||||
isKeyOn[i] = 0;
|
isKeyOn[i] = 0;
|
||||||
isPedaled[i] = 0;
|
isPedaled[i] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawWhiteKeys() {
|
function drawWhiteKeys() {
|
||||||
|
const {velocityMode, rainbowMode} = state;
|
||||||
let wIndex = 0; // white key index
|
let wIndex = 0; // white key index
|
||||||
stroke(0, 0, 0);
|
stroke(0, 0, 0);
|
||||||
strokeWeight(1);
|
strokeWeight(1);
|
||||||
for (let i = 21; i < 109; i++) {
|
for (let i = 21; i < 109; i++) {
|
||||||
if (isBlack[i % 12] == 0) {
|
if (isBlack[i % 12] == 0) {
|
||||||
// it's a white key
|
// it's a white key
|
||||||
if (isKeyOn[i] == 1 && !rainbowMode) {
|
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
|
fill(keyOnColor); // keypressed
|
||||||
} else if (isKeyOn[i] == 1 && rainbowMode) {
|
} else if (isKeyOn[i] && rainbowMode) {
|
||||||
fill(map(i, 21, 108, 0, 1080) % 360, 100, 100, 100); // rainbowMode
|
fill(map(i, 21, 108, 0, 1080) % 360, 100, 100, 100); // rainbowMode
|
||||||
} else if (isPedaled[i] == 1 && !rainbowMode) {
|
} else if (isPedaled[i] && !rainbowMode) {
|
||||||
fill(pedaledColor); // pedaled
|
fill(pedaledColor); // pedaled
|
||||||
} else if (isPedaled[i] == 1 && rainbowMode) {
|
} else if (isPedaled[i] && rainbowMode) {
|
||||||
fill(map(i, 21, 108, 0, 1080) % 360, 100, 70, 100); // pedaled rainbowMode
|
fill(map(i, 21, 108, 0, 1080) % 360, 100, 70, 100); // pedaled rainbowMode
|
||||||
} else {
|
} else {
|
||||||
fill(0, 0, 100); // white key
|
fill(0, 0, 100); // white key
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let thisX = border + wIndex * (whiteKeyWidth + whiteKeySpace);
|
let thisX = border + wIndex * (whiteKeyWidth + whiteKeySpace);
|
||||||
rect(thisX, keyAreaY, whiteKeyWidth, keyAreaHeight, radius);
|
rect(thisX, keyAreaY, whiteKeyWidth, keyAreaHeight, radius);
|
||||||
// println(wIndex);
|
// println(wIndex);
|
||||||
|
@ -69,6 +84,7 @@ function drawWhiteKeys() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawBlackKeys() {
|
function drawBlackKeys() {
|
||||||
|
const {rainbowMode, velocityMode} = state;
|
||||||
let wIndex = 0; // white key index
|
let wIndex = 0; // white key index
|
||||||
stroke(0, 0, 0);
|
stroke(0, 0, 0);
|
||||||
strokeWeight(1.5);
|
strokeWeight(1.5);
|
||||||
|
@ -80,16 +96,28 @@ function drawBlackKeys() {
|
||||||
|
|
||||||
if (isBlack[i % 12] > 0) {
|
if (isBlack[i % 12] > 0) {
|
||||||
// it's a black key
|
// it's a black key
|
||||||
if (isKeyOn[i] == 1 && !rainbowMode) {
|
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
|
fill(keyOnColor); // keypressed
|
||||||
} else if (isKeyOn[i] == 1 && rainbowMode) {
|
} else if (isKeyOn[i] && rainbowMode) {
|
||||||
fill(map(i, 21, 108, 0, 1080) % 360, 100, 100, 100); // rainbowMode
|
fill(map(i, 21, 108, 0, 1080) % 360, 100, 100, 100); // rainbowMode
|
||||||
} else if (isPedaled[i] == 1 && !rainbowMode) {
|
} else if (isPedaled[i] && !rainbowMode) {
|
||||||
fill(pedaledColor); // pedaled
|
fill(pedaledColor); // pedaled
|
||||||
} else if (isPedaled[i] == 1 && rainbowMode) {
|
} else if (isPedaled[i] && rainbowMode) {
|
||||||
fill(map(i, 21, 108, 0, 1080) % 360, 100, 70, 100); // pedaled rainbowMode
|
fill(map(i, 21, 108, 0, 1080) % 360, 100, 70, 100); // pedaled rainbowMode
|
||||||
} else {
|
} else {
|
||||||
fill(0, 0, 0); // white key
|
fill(0, 0, 0); // black key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let thisX = border + (wIndex - 1) * (whiteKeyWidth + whiteKeySpace) + isBlack[i % 12];
|
let thisX = border + (wIndex - 1) * (whiteKeyWidth + whiteKeySpace) + isBlack[i % 12];
|
||||||
|
@ -107,19 +135,19 @@ function drawTexts() {
|
||||||
textAlign(LEFT, TOP);
|
textAlign(LEFT, TOP);
|
||||||
|
|
||||||
// TIME
|
// TIME
|
||||||
let timeText = "TIME" + "\n" + calculateSessionTime();
|
let timeText = 'TIME' + '\n' + calculateSessionTime();
|
||||||
text(timeText, 5, 79);
|
text(timeText, 5, 79);
|
||||||
|
|
||||||
// PEDAL
|
// PEDAL
|
||||||
let pedalText = "PEDALS" + "\nL " + convertNumberToBars(cc67now) + " R " + convertNumberToBars(cc64now)
|
let pedalText = 'PEDALS' + '\nL ' + convertNumberToBars(cc67now) + ' R ' + convertNumberToBars(cc64now)
|
||||||
text(pedalText, 860, 79);
|
text(pedalText, 860, 79);
|
||||||
|
|
||||||
// NOTES
|
// NOTES
|
||||||
let notesText = "NOTE COUNT" + "\n" + totalNotesPlayed;
|
let notesText = 'NOTE COUNT' + '\n' + totalNotesPlayed;
|
||||||
text(notesText, 85, 79);
|
text(notesText, 85, 79);
|
||||||
|
|
||||||
// CALORIES
|
// CALORIES
|
||||||
let caloriesText = "CALORIES" + "\n" + (totalIntensityScore / 250).toFixed(3); // 250 Intensity = 1 kcal.
|
let caloriesText = 'CALORIES' + '\n' + (totalIntensityScore / 250).toFixed(3); // 250 Intensity = 1 kcal.
|
||||||
text(caloriesText, 350, 79);
|
text(caloriesText, 350, 79);
|
||||||
|
|
||||||
// SHORT-TERM DENSITY
|
// SHORT-TERM DENSITY
|
||||||
|
@ -127,19 +155,20 @@ function drawTexts() {
|
||||||
if (shortTermDensity > notesSMax) {
|
if (shortTermDensity > notesSMax) {
|
||||||
notesSMax = shortTermDensity
|
notesSMax = shortTermDensity
|
||||||
};
|
};
|
||||||
let shortTermDensityText = "NPS(MAX)" + "\n" + shortTermDensity + " (" + notesSMax + ")";
|
let shortTermDensityText = 'NPS(MAX)' + '\n' + shortTermDensity + ' (' + notesSMax + ')';
|
||||||
text(shortTermDensityText, 190, 79);
|
text(shortTermDensityText, 190, 79);
|
||||||
|
|
||||||
// LEGATO SCORE
|
// LEGATO SCORE
|
||||||
let legatoScore = legatoHistory.reduce((accumulator, currentValue) => accumulator + currentValue, 0)
|
let legatoScore = legatoHistory.reduce((accumulator, currentValue) => accumulator + currentValue, 0)
|
||||||
legatoScore /= 60;
|
legatoScore /= 60;
|
||||||
let legatoText = "LEGATO" + "\n" + legatoScore.toFixed(2);
|
let legatoText = 'LEGATO' + '\n' + legatoScore.toFixed(2);
|
||||||
text(legatoText, 276, 79);
|
text(legatoText, 276, 79);
|
||||||
|
|
||||||
// NOW PLAYING
|
// NOW PLAYING
|
||||||
let chordSymbol = Tonal.Chord.detect(getPressedKeys(false), { assumePerfectFifth: true })
|
/** @type {string[]} */
|
||||||
let chordSymbolWithoutM = chordSymbol.map((str) => str.replace(/M($|(?=\/))/g, "")); // get rid of the M's
|
let chordSymbol = Tonal.Chord.detect(getPressedKeys(), { assumePerfectFifth: true })
|
||||||
let nowPlayingText = truncateString(getPressedKeys(true), 47) + "\n" + truncateString(chordSymbolWithoutM.join(' '), 47);
|
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);
|
text(nowPlayingText, 440, 79);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,12 +176,11 @@ function pushHistories() {
|
||||||
shortTermTotal.push(notesThisFrame);
|
shortTermTotal.push(notesThisFrame);
|
||||||
shortTermTotal.shift();
|
shortTermTotal.shift();
|
||||||
notesThisFrame = 0;
|
notesThisFrame = 0;
|
||||||
legatoHistory.push(isKeyOn.reduce((accumulator, currentValue) => accumulator + currentValue, 0));
|
legatoHistory.push(isKeyOn.reduce((accumulator, currentValue) => accumulator + !!currentValue, 0));
|
||||||
legatoHistory.shift();
|
legatoHistory.shift();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number} number */
|
||||||
function convertNumberToBars(number) {
|
function convertNumberToBars(number) {
|
||||||
if (number < 0 || number > 127) {
|
if (number < 0 || number > 127) {
|
||||||
throw new Error('Number must be between 0 and 127');
|
throw new Error('Number must be between 0 and 127');
|
||||||
|
@ -164,27 +192,26 @@ function convertNumberToBars(number) {
|
||||||
// Calculate the number of bars
|
// Calculate the number of bars
|
||||||
const numberOfBars = Math.ceil(number / scaleFactor);
|
const numberOfBars = Math.ceil(number / scaleFactor);
|
||||||
|
|
||||||
// Create a string with the calculated number of "|" characters
|
// Create a string with the calculated number of '|' characters
|
||||||
const barString = '|'.repeat(numberOfBars);
|
const barString = '|'.repeat(numberOfBars);
|
||||||
|
|
||||||
// Calculate the number of "." characters required to fill the remaining space
|
// Calculate the number of '.' characters required to fill the remaining space
|
||||||
const numberOfDots = maxBars - numberOfBars;
|
const numberOfDots = maxBars - numberOfBars;
|
||||||
|
|
||||||
// Create a string with the calculated number of "." characters
|
// Create a string with the calculated number of '.' characters
|
||||||
const dotString = '.'.repeat(numberOfDots);
|
const dotString = '.'.repeat(numberOfDots);
|
||||||
|
|
||||||
// Combine the "|" and "." strings
|
// Combine the '|' and '.' strings
|
||||||
const combinedString = barString + dotString;
|
const combinedString = barString + dotString;
|
||||||
|
|
||||||
return combinedString;
|
return combinedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPressedKeys(returnString = true) {
|
function getPressedKeys() {
|
||||||
let pressedOrPedaled = [];
|
let pressedOrPedaled = [];
|
||||||
|
|
||||||
for (let i = 0; i < isKeyOn.length; i++) {
|
for (let i = 0; i < isKeyOn.length; i++) {
|
||||||
pressedOrPedaled[i] = isKeyOn[i] === 1 || isPedaled[i] === 1 ? 1 : 0;
|
pressedOrPedaled[i] = isKeyOn[i] || isPedaled[i];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; // default if sharp
|
let noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; // default if sharp
|
||||||
|
@ -196,25 +223,20 @@ function getPressedKeys(returnString = true) {
|
||||||
const pressedKeys = [];
|
const pressedKeys = [];
|
||||||
|
|
||||||
for (let i = 0; i < pressedOrPedaled.length; i++) {
|
for (let i = 0; i < pressedOrPedaled.length; i++) {
|
||||||
if (pressedOrPedaled[i] === 1) {
|
if (pressedOrPedaled[i]) {
|
||||||
const noteName = noteNames[i % 12];
|
const noteName = noteNames[i % 12];
|
||||||
const octave = Math.floor(i / 12) - 1;
|
const octave = Math.floor(i / 12) - 1;
|
||||||
pressedKeys.push(`${noteName}${octave}`);
|
pressedKeys.push(`${noteName}${octave}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (returnString == true){
|
|
||||||
return pressedKeys.join(' ');
|
|
||||||
} else {
|
|
||||||
return pressedKeys;
|
return pressedKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/** @param {string} str */
|
||||||
|
|
||||||
function truncateString(str, maxLength = 40) {
|
function truncateString(str, maxLength = 40) {
|
||||||
if (str.length <= maxLength) {
|
if (str.length <= maxLength) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
return str.slice(0, maxLength - 3) + '...';
|
return str.slice(0, maxLength - 3) + '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,6 +255,7 @@ function mouseClicked() {
|
||||||
const fileName = `nicechord-pianometer-${strDate}_${strTime}`;
|
const fileName = `nicechord-pianometer-${strDate}_${strTime}`;
|
||||||
saveCanvas(fileName, 'png');
|
saveCanvas(fileName, 'png');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mouseY > 76) {
|
if (mouseY > 76) {
|
||||||
if (mouseX <= 84) {
|
if (mouseX <= 84) {
|
||||||
sessionStartTime = new Date();
|
sessionStartTime = new Date();
|
||||||
|
|
10
style.css
10
style.css
|
@ -552,7 +552,7 @@ input[type=checkbox] {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
label[for=rainbow-mode-checkbox] {
|
label.custom-checkbox {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
|
@ -563,7 +563,7 @@ label[for=rainbow-mode-checkbox] {
|
||||||
transition: .3s;
|
transition: .3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
label[for=rainbow-mode-checkbox]:after {
|
label.custom-checkbox:after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1.25px;
|
top: 1.25px;
|
||||||
|
@ -575,15 +575,15 @@ label[for=rainbow-mode-checkbox]:after {
|
||||||
transition: .3s;
|
transition: .3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + label[for=rainbow-mode-checkbox] {
|
input:checked + label.custom-checkbox {
|
||||||
background: #6f42c1;
|
background: #6f42c1;
|
||||||
}
|
}
|
||||||
|
|
||||||
label[for=rainbow-mode-checkbox]:active:after {
|
label.custom-checkbox:active:after {
|
||||||
width: 32.5px;
|
width: 32.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + label[for=rainbow-mode-checkbox]:after {
|
input:checked + label.custom-checkbox:after {
|
||||||
left: calc(100% - 1.25px);
|
left: calc(100% - 1.25px);
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
}
|
}
|
Loading…
Reference in a new issue