Skip to content

Commit

Permalink
feat: add velocity meter display for piano keys
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
shianyow committed Nov 6, 2024
1 parent 4122760 commit 9d88644
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 9 deletions.
23 changes: 20 additions & 3 deletions globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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);
}
}

function toggleDisplayVelocity(cb) {
displayVelocity = cb.checked;
}
7 changes: 7 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ <h5>選擇 MIDI 裝置</h5>
<span class="switch-txt" turnOn="On" turnOff="Off"></span>
</label>
</div>
<div style="display: flex; align-items: center;">
<span style="margin-right: 5px;">顯示音量</span>
<input type="checkbox" id="display-velocity-checkbox" onclick="toggleDisplayVelocity(this)">
<label for="display-velocity-checkbox">
<span class="switch-txt" turnOn="On" turnOff="Off"></span>
</label>
</div>
</div>
</div>
</div>
Expand Down
94 changes: 93 additions & 1 deletion piano-visualizer.js
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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();
}
Expand Down Expand Up @@ -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;
}
}
}
}
15 changes: 10 additions & 5 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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%);
}

0 comments on commit 9d88644

Please sign in to comment.