上傳檔案到「/」

This commit is contained in:
2026-04-21 10:57:05 +00:00
commit c389659ac9

259
midi_piano_viz.py Normal file
View File

@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""
MIDI Piano Visualizer (AA version)
Supersampling anti-aliasing for smooth rounded keys.
Dependencies:
pip install pygame mido python-rtmidi
Run:
python midi_piano_viz.py
"""
import sys
import colorsys
import pygame
import mido
# ---------- 可調參數(對齊原版) ----------
IS_BLACK = [0, 11, 0, 13, 0, 0, 11, 0, 12, 0, 13, 0]
BORDER = 3
WHITE_KEY_WIDTH = 20
WHITE_KEY_SPACE = 1
BLACK_KEY_WIDTH = 17
BLACK_KEY_HEIGHT = 45
RADIUS = 5
B_RADIUS = 4
KEY_AREA_Y = 3
KEY_AREA_HEIGHT = 70
RAINBOW_MODE = False
NOTE_NAMES = False
KEY_ON_HSB = (326, 100, 100)
PEDALED_HSB = (326, 100, 70)
WIN_WIDTH = (WHITE_KEY_WIDTH + WHITE_KEY_SPACE) * 52 + (BORDER * 2)
WIN_HEIGHT = KEY_AREA_HEIGHT + (KEY_AREA_Y * 2)
BG_HSB = (0, 0, 30)
FPS = 60
# 超取樣倍率3x 已經很漂亮4x 更絲滑但稍微吃 CPU
SUPERSAMPLE = 3
# ---------- 工具 ----------
def hsb_to_rgb(h, s, v):
r, g, b = colorsys.hsv_to_rgb(h / 360.0, s / 100.0, v / 100.0)
return (int(r * 255), int(g * 255), int(b * 255))
def rainbow_color(pitch, value=100):
hue = ((pitch - 21) / (108 - 21) * 1080) % 360
return hsb_to_rgb(hue, 100, value)
def pick_midi_input():
ports = mido.get_input_names()
if not ports:
print("找不到任何 MIDI 輸入裝置。請先插上鍵盤再試一次。")
sys.exit(1)
print("\nAvailable MIDI inputs:")
for i, name in enumerate(ports):
print(f" [{i}] {name}")
if len(ports) == 1:
print(f"\n只有一個裝置,自動選擇 [0] {ports[0]}")
return ports[0]
while True:
try:
choice = input("\n選擇要使用的 MIDI 輸入編號:").strip()
idx = int(choice)
if 0 <= idx < len(ports):
return ports[idx]
except (ValueError, KeyboardInterrupt, EOFError):
print("再來一次。")
# ---------- 狀態 ----------
class State:
def __init__(self):
self.is_key_on = [0] * 128
self.is_pedaled = [0] * 128
self.now_pedaling = False
def note_on(self, pitch, velocity):
if velocity == 0:
self.note_off(pitch)
return
self.is_key_on[pitch] = 1
if self.now_pedaling:
self.is_pedaled[pitch] = 1
def note_off(self, pitch):
self.is_key_on[pitch] = 0
def control_change(self, number, value):
if number == 64:
if value >= 64:
self.now_pedaling = True
for i in range(128):
self.is_pedaled[i] = self.is_key_on[i]
else:
self.now_pedaling = False
for i in range(128):
self.is_pedaled[i] = 0
# ---------- 繪製(在放大的 surface 上畫,最後縮小) ----------
def draw_rounded_rect(surface, color, rect, radius, stroke_color=None, stroke_width=0):
pygame.draw.rect(surface, color, rect, border_radius=radius)
if stroke_color is not None and stroke_width > 0:
pygame.draw.rect(surface, stroke_color, rect, width=stroke_width, border_radius=radius)
def draw_scene(big_surface, state, font_big):
s = SUPERSAMPLE
border = BORDER * s
wkw = WHITE_KEY_WIDTH * s
wks = WHITE_KEY_SPACE * s
bkw = BLACK_KEY_WIDTH * s
bkh = BLACK_KEY_HEIGHT * s
radius = RADIUS * s
bradius = B_RADIUS * s
kay = KEY_AREA_Y * s
kah = KEY_AREA_HEIGHT * s
key_on_rgb = hsb_to_rgb(*KEY_ON_HSB)
pedaled_rgb = hsb_to_rgb(*PEDALED_HSB)
white_rgb = (255, 255, 255)
black_rgb = (0, 0, 0)
stroke_rgb = (0, 0, 0)
bg_rgb = hsb_to_rgb(*BG_HSB)
big_surface.fill(bg_rgb)
# --- 白鍵 ---
w_index = 0
for i in range(21, 109):
if IS_BLACK[i % 12] == 0:
if state.is_key_on[i] == 1 and not RAINBOW_MODE:
color = key_on_rgb
elif state.is_key_on[i] == 1 and RAINBOW_MODE:
color = rainbow_color(i, 100)
elif state.is_pedaled[i] == 1 and not RAINBOW_MODE:
color = pedaled_rgb
elif state.is_pedaled[i] == 1 and RAINBOW_MODE:
color = rainbow_color(i, 70)
else:
color = white_rgb
x = border + w_index * (wkw + wks)
rect = pygame.Rect(x, kay, wkw, kah)
draw_rounded_rect(big_surface, color, rect, radius,
stroke_color=stroke_rgb, stroke_width=max(1, s // 2))
w_index += 1
# --- 黑鍵 ---
w_index = 0
for i in range(21, 109):
if IS_BLACK[i % 12] == 0:
w_index += 1
continue
if state.is_key_on[i] == 1 and not RAINBOW_MODE:
color = key_on_rgb
elif state.is_key_on[i] == 1 and RAINBOW_MODE:
color = rainbow_color(i, 100)
elif state.is_pedaled[i] == 1 and not RAINBOW_MODE:
color = pedaled_rgb
elif state.is_pedaled[i] == 1 and RAINBOW_MODE:
color = rainbow_color(i, 70)
else:
color = black_rgb
x = border + (w_index - 1) * (wkw + wks) + IS_BLACK[i % 12] * s
rect = pygame.Rect(x, kay - 1 * s, bkw, bkh)
draw_rounded_rect(big_surface, color, rect, bradius,
stroke_color=stroke_rgb, stroke_width=max(1, int(s * 0.75)))
# --- 音名 ---
if NOTE_NAMES:
note_names = ["A", "B", "C", "D", "E", "F", "G"]
text_rgb = (64, 64, 64)
w_index = 0
for i in range(52):
x = border + w_index * (wkw + wks)
y = kay + kah - 13 * s
name = note_names[i % 7]
surf = font_big.render(name, True, text_rgb)
rect = surf.get_rect(center=(x + wkw // 2, y))
big_surface.blit(surf, rect)
w_index += 1
# ---------- 主程式 ----------
def main():
port_name = pick_midi_input()
pygame.init()
pygame.display.set_caption("MIDI Piano Visualizer")
screen = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
big_surface = pygame.Surface((WIN_WIDTH * SUPERSAMPLE, WIN_HEIGHT * SUPERSAMPLE))
clock = pygame.time.Clock()
# 字型也要放大版(跟著 big_surface 一起縮小,文字就會 AA
font_big = pygame.font.SysFont(None, 14 * SUPERSAMPLE)
state = State()
print(f"\n開始接收:{port_name}")
print("視窗聚焦時按 R 切換彩虹模式、N 切換音名、ESC 或關閉視窗結束。")
global RAINBOW_MODE, NOTE_NAMES
with mido.open_input(port_name) as port:
running = True
while running:
for msg in port.iter_pending():
if msg.type == "note_on":
state.note_on(msg.note, msg.velocity)
elif msg.type == "note_off":
state.note_off(msg.note)
elif msg.type == "control_change":
state.control_change(msg.control, msg.value)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_r:
RAINBOW_MODE = not RAINBOW_MODE
elif event.key == pygame.K_n:
NOTE_NAMES = not NOTE_NAMES
# 1) 在放大 surface 上繪製
draw_scene(big_surface, state, font_big)
# 2) smoothscale 縮回視窗大小 → 邊緣自動柔化
pygame.transform.smoothscale(big_surface, (WIN_WIDTH, WIN_HEIGHT), screen)
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
if __name__ == "__main__":
main()