From 1851ec2258607d93c0f4d33bd2cd703ac7e5e8e6 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 | 122 +++++++++++++++++++++++++++++++++++++++++++- style.css | 15 ++++-- 4 files changed, 158 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..ca53b48 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,113 @@ 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 drawVelocityBars(isBlackKey, isPeak = false) { + let wIndex = 0; + for (let i = 21; i < 109; i++) { + const velocityValue = isPeak ? peakVelocities[i] : velocities[i]; + + if (velocityValue > 0 && (isBlack[i % 12] > 0 === isBlackKey)) { + const { x, width } = getKeyPosition(i, wIndex); + + // 根據是否為最大音量線和黑白鍵來決定顏色 + const barColor = isBlackKey ? + color(120, 90, isPeak ? 85 : 60, isPeak ? 100 : 90) : // 黑鍵顏色 + color(120, 90, isPeak ? 100 : 75, isPeak ? 100 : 90); // 白鍵顏色 + + if (isPeak) { + // 繪製最大音量線 + drawBar(x, width, getYPosition(velocityValue, true), barColor); + } else { + // 繪製當前音量條 + for (let j = 0; j < Math.ceil(velocityValue * 10); j++) { + drawBar(x, width, getYPosition(j), 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. 繪製當前音量(白鍵) + drawVelocityBars(false, false); + + // 3. 繪製當前音量(黑鍵) + drawVelocityBars(true, false); + + // 4. 繪製最大音量線(白鍵) + drawVelocityBars(false, true); + + // 5. 繪製最大音量線(黑鍵) + drawVelocityBars(true, 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]--; + } + // 當保持時間結束,且既沒有按著鍵也沒有踩踏板時,清除最大音量線 + 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