From 9d8864499877e1565f086c8c659374cd4f91379e Mon Sep 17 00:00:00 2001 From: Shian-Yow Wu Date: Wed, 6 Nov 2024 04:20:22 +0800 Subject: [PATCH] feat: add velocity meter display for piano keys The velocity meter provides visual feedback for key press intensity, helping users understand their playing dynamics. Bars use scale marks for better readability, and peak indicators help track maximum velocity for each note. --- globals.js | 23 +++++++++-- index.html | 7 ++++ piano-visualizer.js | 94 ++++++++++++++++++++++++++++++++++++++++++++- style.css | 15 +++++--- 4 files changed, 130 insertions(+), 9 deletions(-) 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