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