Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add velocity meter display for piano keys #6

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = 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);
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
122 changes: 121 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, 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]
Expand All @@ -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();
}
Expand Down Expand Up @@ -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;
}
}
}

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%);
}