diff --git a/globals.js b/globals.js
index 4b4c5a0..a91e3be 100644
--- a/globals.js
+++ b/globals.js
@@ -38,6 +38,15 @@ let totalIntensityScore = 0;
let notePressedCount = 0;
let notePressedCountHistory = [];
+// 音量相關的全域變數
+let velocities = new Array(128).fill(0); // 儲存每個音符的當前音量
+let velocityDecayRate = 0.98; // 調整衰減速率
+let maxVelocityHeight = 30; // 調整最大高度
+let peakVelocities = new Array(128).fill(0); // 儲存每個音符的最大音量
+let peakHoldTime = new Array(128).fill(0); // 儲存最大音量的持續時間
+const PEAK_HOLD_FRAMES = 180; // 3 秒 = 60 幀/秒 × 3 秒
+let displayVelocity = false; // 改為預設關閉
+
WebMidi.enable(function (err) { //check if WebMidi.js is enabled
if (err) {
console.log("WebMidi could not be enabled.", err);
@@ -91,11 +100,15 @@ function noteOn(pitch, velocity) {
totalNotesPlayed++;
notesThisFrame++;
totalIntensityScore += velocity;
+
+ // 更新按鍵音量和最大音量線
+ velocities[pitch] = velocity;
+ peakVelocities[pitch] = velocity;
+ peakHoldTime[pitch] = PEAK_HOLD_FRAMES;
- // piano visualizer
isKeyOn[pitch] = 1;
if (nowPedaling) {
- isPedaled[pitch] = 1;
+ isPedaled[pitch] = 1;
}
}
@@ -146,4 +159,8 @@ function changeColor() {
darkenedColor = keyOnColor.levels.map(x => floor(x * .7));
pedaledColor = color(`rgb(${darkenedColor[0]}, ${darkenedColor[1]}, ${darkenedColor[2]})`)
console.log(pedaledColor.levels);
-}
\ No newline at end of file
+}
+
+function toggleDisplayVelocity(cb) {
+ displayVelocity = cb.checked;
+}
diff --git a/index.html b/index.html
index b1b32ea..940bd5c 100644
--- a/index.html
+++ b/index.html
@@ -42,6 +42,13 @@
選擇 MIDI 裝置
+
+ 顯示音量
+
+
+
diff --git a/piano-visualizer.js b/piano-visualizer.js
index 39c4c8c..c79d717 100644
--- a/piano-visualizer.js
+++ b/piano-visualizer.js
@@ -1,5 +1,5 @@
function setup() {
- createCanvas(1098, 118).parent('piano-visualizer');
+ createCanvas(1098, 190).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]
@@ -12,8 +12,18 @@ function setup() {
function draw() {
background(0, 0, 20, 100);
pushHistories();
+
+ // 將所有元素往下移動固定距離,給音量條預留空間
+ translate(0, 70); // 改為固定往下移動 70
+
drawWhiteKeys();
drawBlackKeys();
+
+ updateVelocities();
+ if (displayVelocity) {
+ drawVelocityBars();
+ }
+
if (displayNoteNames) {drawNoteNames();};
drawTexts();
}
@@ -273,3 +283,85 @@ function mouseClicked() {
}
console.log(mouseX, mouseY);
}
+
+function drawVelocityBars() {
+ noStroke();
+ let wIndex = 0;
+
+ for (let i = 21; i < 109; i++) {
+ if (velocities[i] > 0 || peakVelocities[i] > 0) {
+ // 計算位置
+ let x, width;
+ if (isBlack[i % 12] == 0) {
+ x = border + wIndex * (whiteKeyWidth + whiteKeySpace);
+ width = whiteKeyWidth;
+ wIndex++;
+ } else {
+ x = border + (wIndex - 1) * (whiteKeyWidth + whiteKeySpace) + isBlack[i % 12];
+ width = blackKeyWidth;
+ }
+
+ // 繪製背景刻度
+ for (let j = 0; j < 10; j++) {
+ fill(0, 0, 50, 30);
+ let y = keyAreaY - (j + 1) * (maxVelocityHeight * 2 / 10);
+ rect(x, y, width, 1);
+ }
+
+ // 繪製當前音量刻度
+ if (velocities[i] > 0) {
+ let totalBars = Math.ceil(velocities[i] * 10); // 將音量轉換為刻度數量
+ for (let j = 0; j < totalBars; j++) {
+ fill(120, 100, 80, 90);
+ let y = keyAreaY - (j + 1) * (maxVelocityHeight * 2 / 10);
+ rect(x, y, width, 1);
+ }
+ }
+
+ // 繪製最大音量線
+ if (peakVelocities[i] > 0) {
+ let peakHeight = peakVelocities[i] * maxVelocityHeight * 2;
+ stroke(120, 100, 100);
+ strokeWeight(2);
+ line(x, keyAreaY - peakHeight, x + width, keyAreaY - peakHeight);
+ noStroke();
+ }
+
+ } else if (isBlack[i % 12] == 0) {
+ wIndex++;
+ }
+ }
+}
+
+function updateVelocities() {
+ for (let i = 0; i < velocities.length; i++) {
+ if (velocities[i] > 0) {
+ // 更新最大音量
+ if (velocities[i] > peakVelocities[i]) {
+ peakVelocities[i] = velocities[i];
+ peakHoldTime[i] = PEAK_HOLD_FRAMES;
+ }
+
+ // 按鍵放開且沒有踩踏板時,快速降低音量
+ if (!isKeyOn[i] && !isPedaled[i]) {
+ velocities[i] *= 0.5; // 快速衰減
+ } else {
+ velocities[i] *= 0.995; // 按著或踩踏板時的緩慢衰減
+ }
+
+ // 如果音量太小就設為 0
+ if (velocities[i] < 0.01) {
+ velocities[i] = 0;
+ }
+ }
+
+ // 更新最大音量線
+ if (peakHoldTime[i] > 0) {
+ peakHoldTime[i]--;
+ if (peakHoldTime[i] <= 0) {
+ // 時間到後直接清除最大音量線
+ peakVelocities[i] = 0;
+ }
+ }
+ }
+}
diff --git a/style.css b/style.css
index 1ea1a12..7019370 100644
--- a/style.css
+++ b/style.css
@@ -553,7 +553,8 @@ input[type=checkbox] {
}
label[for=rainbow-mode-checkbox],
-label[for=display-note-names-checkbox] {
+label[for=display-note-names-checkbox],
+label[for=display-velocity-checkbox] {
cursor: pointer;
width: 50px;
height: 25px;
@@ -565,7 +566,8 @@ label[for=display-note-names-checkbox] {
}
label[for=rainbow-mode-checkbox]:after,
-label[for=display-note-names-checkbox]:after {
+label[for=display-note-names-checkbox]:after,
+label[for=display-velocity-checkbox]:after {
content: '';
position: absolute;
top: 1.25px;
@@ -578,17 +580,20 @@ label[for=display-note-names-checkbox]:after {
}
input:checked + label[for=rainbow-mode-checkbox],
-input:checked + label[for=display-note-names-checkbox] {
+input:checked + label[for=display-note-names-checkbox],
+input:checked + label[for=display-velocity-checkbox] {
background: #6f42c1;
}
label[for=rainbow-mode-checkbox]:active:after,
-label[for=display-note-names-checkbox]:active:after {
+label[for=display-note-names-checkbox]:active:after,
+label[for=display-velocity-checkbox]:active:after {
width: 32.5px;
}
input:checked + label[for=rainbow-mode-checkbox]:after,
-input:checked + label[for=display-note-names-checkbox]:after {
+input:checked + label[for=display-note-names-checkbox]:after,
+input:checked + label[for=display-velocity-checkbox]:after {
left: calc(100% - 1.25px);
transform: translateX(-100%);
}
\ No newline at end of file