From 577fdade2c8668a3e727feef4f1b0ec103409f8b 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 | 129 +++++++++++++++++++++++++++++++++++++++++++- style.css | 15 ++++-- 4 files changed, 165 insertions(+), 9 deletions(-) diff --git a/globals.js b/globals.js index 4b4c5a0..1e155ee 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 = 40; // 調整最大高度 +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..c97786c 100644 --- a/piano-visualizer.js +++ b/piano-visualizer.js @@ -1,5 +1,5 @@ function setup() { - createCanvas(1098, 118).parent('piano-visualizer'); + createCanvas(1098, 210).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, 90); // 改為固定往下移動 90 + drawWhiteKeys(); drawBlackKeys(); + + updateVelocities(); + if (displayVelocity) { + drawVelocityBars(); + } + if (displayNoteNames) {drawNoteNames();}; drawTexts(); } @@ -273,3 +283,120 @@ function mouseClicked() { } console.log(mouseX, mouseY); } + +function drawVelocityBars() { + // 輔助函數:計算x和寬度 + function getKeyPosition(i, wIndex) { + const isBlackKey = isBlack[i % 12] > 0; + if (!isBlackKey) { + return { + x: border + wIndex * (whiteKeyWidth + whiteKeySpace), + width: whiteKeyWidth + }; + } + return { + x: border + (wIndex - 1) * (whiteKeyWidth + whiteKeySpace) + isBlack[i % 12], + width: blackKeyWidth + }; + } + + // 輔助函數:繪製單個音量條 + function drawBar(x, width, y, color) { + noStroke(); + fill(color); + rect(x, y, width, 2); + } + + // 輔助函數:計算y位置 + function getYPosition(value, isMaxLine = false) { + const multiplier = isMaxLine ? Math.ceil(value * 10) : (value + 1); + return keyAreaY - multiplier * (maxVelocityHeight * 2 / 10); + } + + // 輔助函數:繪製當前音量 + function drawCurrentVelocity(isBlackKey) { + let wIndex = 0; + for (let i = 21; i < 109; i++) { + // 修正:使用 > 0 來判斷黑鍵 + if (velocities[i] > 0 && (isBlack[i % 12] > 0 === isBlackKey)) { + const { x, width } = getKeyPosition(i, wIndex); + const barColor = isBlackKey ? + color(120, 90, 60, 90) : // 黑鍵顏色 + color(120, 90, 75, 90); // 白鍵顏色 + + for (let j = 0; j < Math.ceil(velocities[i] * 10); j++) { + drawBar(x, width, getYPosition(j), barColor); + } + } + if (!isBlack[i % 12]) wIndex++; + } + } + + // 輔助函數:繪製最大音量線 + function drawPeakVelocity(isBlackKey) { + let wIndex = 0; + for (let i = 21; i < 109; i++) { + if (peakVelocities[i] > 0 && (isBlack[i % 12] > 0 === isBlackKey)) { + const { x, width } = getKeyPosition(i, wIndex); + const barColor = isBlackKey ? + color(120, 90, 85) : // 黑鍵顏色 + color(120, 90, 100); // 白鍵顏色 + + drawBar(x, width, getYPosition(peakVelocities[i], true), barColor); + } + if (!isBlack[i % 12]) wIndex++; + } + } + + // 1. 繪製背景刻度 + let wIndex = 0; + for (let i = 21; i < 109; i++) { + if (velocities[i] > 0 || peakVelocities[i] > 0) { + const { x, width } = getKeyPosition(i, wIndex); + for (let j = 0; j < 10; j++) { + drawBar(x, width, getYPosition(j), color(0, 0, 50, 30)); + } + } + if (!isBlack[i % 12]) wIndex++; + } + + // 2. 繪製當前音量(白鍵) + drawCurrentVelocity(false); + + // 3. 繪製當前音量(黑鍵) + drawCurrentVelocity(true); + + // 4. 繪製最大音量線(白鍵) + drawPeakVelocity(false); + + // 5. 繪製最大音量線(黑鍵) + drawPeakVelocity(true); +} + +function updateVelocities() { + for (let i = 0; i < velocities.length; i++) { + if (velocities[i] > 0) { + // 按鍵放開且沒有踩踏板時,快速降低音量 + 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]--; + } + // 當按鍵和踏板都放開,且 peakHoldTime 已結束時,立即清除最大音量線 + if (peakHoldTime[i] <= 0 && !isKeyOn[i] && !isPedaled[i]) { + 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