import os
# Pydroid3/Android 用 SDL オーディオドライバ
os.environ['SDL_AUDIODRIVER'] = 'android'
import tkinter as tk
from tkinter import font
import time
import threading
import math
import numpy as np
import pygame
from mido import MidiFile
class PianoApp:
def __init__(self, root, midi_path):
self.root = root
self.root.title("スクリャービン練習曲Op.8-12:デモ")
# pygame.mixer 初期化
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
self.sound_cache = {}
# フォント
self.title_font = font.Font(family="Helvetica", size=16, weight="bold")
self.key_font = font.Font(family="Helvetica", size=10)
# ウィンドウサイズ
w = min(1000, root.winfo_screenwidth())
h = min(400, root.winfo_screenheight())
root.geometry(f"{w}x{h+100}")
# タイトル
tk.Label(root,
text="スクリャービン練習曲Op.8-12",
font=self.title_font).pack(pady=10)
# Canvas
self.canvas = tk.Canvas(root, width=w, height=h, bg="gray")
self.canvas.pack()
# MIDI からノートデータを生成
self.rachmaninoff_data = self.parse_midi_to_data(midi_path)
# 再生/停止ボタン
frame = tk.Frame(root)
frame.pack(pady=5)
tk.Button(frame, text="再生", command=self.start_playback, font=self.key_font).pack(side=tk.LEFT, padx=5)
tk.Button(frame, text="停止", command=self.stop_playback, font=self.key_font).pack(side=tk.LEFT, padx=5)
# 鍵盤サイズ
self.white_w = 40
self.white_h = h
self.black_w = 25
self.black_h = int(h * 0.6)
# Canvas item ID → note/color マッピング
self.id_to_note = {}
self.id_to_color = {}
# 鍵盤を描画
self.draw_keys(start_midi=36, num_keys=36)
# デモ再生用スレッド
self.playback_thread = None
self.stop_flag = False
# クリックで音を鳴らす
self.canvas.bind("<Button-1>", self.on_click)
def parse_midi_to_data(self, path):
mid = MidiFile(path)
events = []
note_on_times = {}
current = 0.0
for msg in mid:
current += msg.time
if not msg.is_meta:
if msg.type == 'note_on' and msg.velocity > 0:
note_on_times.setdefault(msg.note, []).append(current)
elif msg.type in ('note_off',) or (msg.type=='note_on' and msg.velocity==0):
lst = note_on_times.get(msg.note)
if lst:
start = lst.pop(0)
dur = current - start
events.append((start, msg.note, dur))
events.sort(key=lambda x: x[0])
return events
def draw_keys(self, start_midi, num_keys):
note_names = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
white_positions = {}
white_count = 0
# ① 白鍵の位置計算
for note in range(start_midi, start_midi+num_keys):
if '#' not in note_names[note % 12]:
x = white_count * self.white_w
white_positions[note] = x
white_count += 1
# ② 白鍵描画
for note, x in white_positions.items():
rect = self.canvas.create_rectangle(
x, 0, x + self.white_w, self.white_h,
fill="white", outline="black"
)
self.id_to_note[rect] = note
self.id_to_color[rect] = "white"
# ③ 黒鍵描画
for note in range(start_midi, start_midi+num_keys):
if '#' in note_names[note % 12]:
prev = note - 1
if prev in white_positions:
x0 = white_positions[prev] + self.white_w - self.black_w//2
rect = self.canvas.create_rectangle(
x0, 0, x0 + self.black_w, self.black_h,
fill="black", outline="black"
)
self.canvas.tag_raise(rect)
self.id_to_note[rect] = note
self.id_to_color[rect] = "black"
def on_click(self, event):
items = self.canvas.find_overlapping(event.x, event.y, event.x, event.y)
if not items:
return
item = items[-1]
note = self.id_to_note.get(item)
if note is not None:
self.play_note(note, 0.5)
def midi_to_freq(self, note):
return 440.0 * (2 ** ((note - 69) / 12.0))
def generate_sound(self, note, duration):
key = (note, duration)
if key in self.sound_cache:
return self.sound_cache[key]
sr = 44100
n = int(duration * sr)
t = np.linspace(0, duration, n, False)
wave = 0.5 * np.sin(2 * math.pi * self.midi_to_freq(note) * t)
audio = np.int16(wave * 32767)
sound = pygame.sndarray.make_sound(audio)
self.sound_cache[key] = sound
return sound
def play_note(self, note, duration):
# 鍵盤ハイライト
for item, n in self.id_to_note.items():
if n == note:
orig = self.id_to_color[item]
self.canvas.itemconfig(item, fill="red")
self.root.after(int(duration*1000), lambda i=item, c=orig: self.canvas.itemconfig(i, fill=c))
# サウンド再生
snd = self.generate_sound(note, duration)
snd.play()
def playback(self):
start = time.time()
self.stop_flag = False
for t, note, dur in self.rachmaninoff_data:
if self.stop_flag:
break
now = time.time() - start
if now < t:
time.sleep(t - now)
# UI スレッドに play_note をキュー
self.root.after(0, lambda n=note, d=dur: self.play_note(n, d))
def start_playback(self):
if self.playback_thread and self.playback_thread.is_alive():
return
self.stop_flag = False
self.playback_thread = threading.Thread(target=self.playback, daemon=True)
self.playback_thread.start()
def stop_playback(self):
self.stop_flag = True
pygame.mixer.stop()
if self.playback_thread:
self.playback_thread.join()
if __name__ == "__main__":
root = tk.Tk()
# 同フォルダに置いたMIDIファイル名を指定してください
app = PianoApp(root, midi_path="e8_12.mid")
root.mainloop()
いろいろあって落ち着いてからBloggerは放置するようになり、10数年ぶりにアカウントが残っているかどうか探しにいったら、更新されずに長年放置されていたということで、自然消滅させられたみたいだ。