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