こんにちは!今回はPythonを使って作成した音楽アプリを紹介します。
リアルタイムで演奏できるピアノと、音楽理論を活かしたコード進行ジェネレーターを搭載。
さらに、作ったコード進行はMIDIファイルとして保存も可能です。
目次
- アプリの概要
- 主な機能紹介
- Python全コード掲載
- 使い方ガイド
- exeファイルのダウンロード
- まとめ&今後の展望
1. アプリの概要
このアプリはPythonのtkinter
でGUIを作り、pygame.midi
でリアルタイムに音を鳴らします。
ピアノ鍵盤を画面に表示し、マウスで演奏可能。
コード進行ジェネレーターは、音楽理論に基づく複数スタイルのコード進行を自動生成。
生成したコード進行はアプリ内で再生し、MIDIファイルとして保存できます。


2. 主な機能紹介
- リアルタイムピアノ
- 白鍵・黒鍵を画面に表示
- 複数の鍵盤同時押しに対応
- 音色(ピアノ、オルガン、ギター等)を選択可能
- コード進行ジェネレーター
- ポップ、ジャズ、ブルースの定番進行対応
- 「スタイル固定」「ランダム」「拡張コード」モードで多様な進行を生成
- 生成した進行のリアルタイム再生
- MIDIファイルで保存可能
3. Python全コード掲載
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pygame.midi
from mido import Message, MidiFile, MidiTrack
import random
# --- MIDI初期化 ---
pygame.midi.init()
player = pygame.midi.Output(0)
player.set_instrument(0)
# --- 音色リスト ---
INSTRUMENTS = {
"ピアノ": 0,
"エレクトリックピアノ": 4,
"オルガン": 16,
"ギター": 24,
"バイオリン": 40,
"トランペット": 56,
"サックス": 64,
"フルート": 73
}
# --- 音階情報(白鍵・黒鍵) ---
WHITE_KEYS = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
BLACK_KEYS = ['C#', 'D#', '', 'F#', 'G#', 'A#', '']
BASE_NOTE = 60 # C4=60
# --- 基本ダイアトニックコード(三和音) ---
DIATONIC_CHORDS = {
"C": ["C", "Dm", "Em", "F", "G", "Am", "Bdim"],
"G": ["G", "Am", "Bm", "C", "D", "Em", "F#dim"],
"Am": ["Am", "Bdim", "C", "Dm", "Em", "F", "G"],
"F": ["F", "Gm", "Am", "Bb", "C", "Dm", "Edim"]
}
# --- 拡張コード例(7th,9th,sus4など)をキー毎に用意 ---
EXTENDED_CHORDS = {
"C": ["Cmaj7", "Dm7", "Em7", "Fmaj7", "G7", "Am7", "Bdim7", "Csus4", "C9", "Dm9", "G13"],
"G": ["Gmaj7", "Am7", "Bm7", "Cmaj7", "D7", "Em7", "F#dim7", "Gsus4", "G9", "Am9", "D13"],
"Am": ["Am7", "Bdim7", "Cmaj7", "Dm7", "Em7", "Fmaj7", "G7", "Asus4", "Am9", "Dm9", "E7"],
"F": ["Fmaj7", "Gm7", "Am7", "Bbmaj7", "C7", "Dm7", "Edim7", "Fsus4", "F9", "Gm9", "C13"]
}
# --- コード進行パターン(ローマ数字) ---
PROGRESSIONS = {
"ポップ": ["I", "V", "vi", "IV"],
"ジャズ": ["ii", "V", "I", "vi"],
"ブルース": ["I", "I", "I", "I", "IV", "IV", "I", "I", "V", "IV", "I", "V"]
}
ROMAN_MAP = {
"I": 0, "ii": 1, "iii": 2, "IV": 3, "V": 4, "vi": 5, "vii°": 6
}
NOTE_MAP = {
"C": 60, "C#": 61, "Db": 61, "D": 62, "D#": 63, "Eb": 63,
"E": 64, "F": 65, "F#": 66, "Gb": 66, "G": 67, "G#": 68, "Ab": 68,
"A": 69, "A#": 70, "Bb": 70, "B": 71
}
# --- 関数群 ---
def white_key_to_semitones(index):
mapping = [0, 2, 4, 5, 7, 9, 11]
return mapping[index]
def black_key_to_semitones(index):
mapping = [1, 3, 6, 8, 10]
black_index_map = [0, 1, -1, 2, 3, 4, -1]
mapped_index = black_index_map[index]
if mapped_index == -1:
return 0
return mapping[mapped_index]
# 和音を解析してMIDIノートリストを返す関数
def chord_to_notes(chord_name):
root = ''
for c in chord_name:
if c.isalpha() or c in ['#', 'b']:
root += c
else:
break
root_note = NOTE_MAP.get(root, 60)
# 簡易コードタイプ判別
if "m7" in chord_name:
intervals = [0, 3, 7, 10] # マイナー7th
elif "7" in chord_name and "maj7" not in chord_name:
intervals = [0, 4, 7, 10] # ドミナント7th
elif "maj7" in chord_name:
intervals = [0, 4, 7, 11] # メジャー7th
elif "dim7" in chord_name:
intervals = [0, 3, 6, 9] # ディミニッシュ7th
elif "dim" in chord_name:
intervals = [0, 3, 6] # ディミニッシュ(三和音)
elif "sus4" in chord_name:
intervals = [0, 5, 7] # サスフォー
elif "9" in chord_name:
intervals = [0, 4, 7, 10, 14] # ドミナント9th(単純化)
elif chord_name.startswith("m"):
intervals = [0, 3, 7] # マイナー三和音
else:
intervals = [0, 4, 7] # メジャー三和音
notes = [root_note + i for i in intervals]
return notes
def play_notes(notes, velocity=100, duration_ms=500):
for n in notes:
player.note_on(n, velocity)
pygame.time.wait(duration_ms)
for n in notes:
player.note_off(n, velocity)
def play_chords(chords, bpm, pattern):
player.set_instrument(INSTRUMENTS.get(piano_tab.instrument_var.get(), 0))
beat_ms = int(60000 / bpm)
for chord in chords:
notes = chord_to_notes(chord)
play_notes(notes, 100, beat_ms)
def save_midi(chords, bpm):
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
beat_ticks = 480
track.append(Message('program_change', program=INSTRUMENTS.get(piano_tab.instrument_var.get(),0), time=0))
for chord in chords:
notes = chord_to_notes(chord)
# 和音全部をトラックに入れる(同時に鳴る)
for i, note in enumerate(notes):
time_val = 0 if i == 0 else 0 # 同時に鳴らすので0でOK
track.append(Message('note_on', note=note, velocity=64, time=time_val))
track.append(Message('note_off', note=notes[0], velocity=64, time=beat_ticks))
for note in notes[1:]:
track.append(Message('note_off', note=note, velocity=64, time=0))
filepath = filedialog.asksaveasfilename(defaultextension=".mid", filetypes=[("MIDIファイル", "*.mid")])
if filepath:
mid.save(filepath)
messagebox.showinfo("保存完了", f"{filepath} に保存しました。")
def roman_to_chord(roman, chords):
return chords[ROMAN_MAP[roman]]
def generate_progression(key, style, mode):
if key not in DIATONIC_CHORDS:
return []
if mode == "スタイル固定":
return [roman_to_chord(r, DIATONIC_CHORDS[key]) for r in PROGRESSIONS.get(style, [])]
elif mode == "拡張コード":
count = random.randint(4, 8)
extended_list = EXTENDED_CHORDS.get(key, DIATONIC_CHORDS[key])
return [random.choice(extended_list) for _ in range(count)]
else:
count = random.randint(4, 8)
return [random.choice(DIATONIC_CHORDS[key]) for _ in range(count)]
# --- タブ1: ピアノ ---
class PianoTab(ttk.Frame):
def __init__(self, master):
super().__init__(master)
self.instrument_var = tk.StringVar(value="ピアノ")
self.current_notes = set()
self.create_widgets()
self.bind_events()
def create_widgets(self):
label = ttk.Label(self, text="音色選択:", font=("Arial", 12))
label.pack(pady=(10, 0))
self.inst_combo = ttk.Combobox(self, textvariable=self.instrument_var,
values=list(INSTRUMENTS.keys()),
state="readonly", font=("Arial", 12))
self.inst_combo.pack(pady=(0,10))
self.inst_combo.bind("<<ComboboxSelected>>", lambda e: self.change_instrument())
self.canvas = tk.Canvas(self, bg="white", highlightthickness=0)
self.canvas.pack(fill="both", expand=True)
self.canvas.bind("<Configure>", self.on_resize)
def on_resize(self, event):
self.canvas.delete("all")
self.draw_keys(event.width, event.height)
def draw_keys(self, width, height):
self.white_key_ids = []
self.black_key_ids = []
self.key_id_to_note = {}
key_count = 7
key_width = width / key_count
key_height = height
black_key_width = key_width * 0.65
black_key_height = height * 0.6
for i, key in enumerate(WHITE_KEYS):
x = i * key_width
key_id = self.canvas.create_rectangle(x, 0, x + key_width, key_height, fill="white", outline="black", width=2)
self.white_key_ids.append(key_id)
midi_note = BASE_NOTE + white_key_to_semitones(i)
self.key_id_to_note[key_id] = midi_note
self.canvas.create_text(x + key_width / 2, key_height - 20, text=key, font=("Arial", int(key_height*0.08)))
for i, key in enumerate(BLACK_KEYS):
if key == '':
continue
x = (i + 1) * key_width - black_key_width / 2
key_id = self.canvas.create_rectangle(x, 0, x + black_key_width, black_key_height, fill="black", outline="black")
self.black_key_ids.append(key_id)
midi_note = BASE_NOTE + black_key_to_semitones(i)
self.key_id_to_note[key_id] = midi_note
self.canvas.create_text(x + black_key_width / 2, black_key_height - 20, text=key, font=("Arial", int(black_key_height*0.08)), fill="white")
def bind_events(self):
self.canvas.tag_bind("all", "<ButtonPress-1>", self.on_press)
self.canvas.tag_bind("all", "<ButtonRelease-1>", self.on_release)
def on_press(self, event):
key_id = self.find_key(event.x, event.y)
if key_id and key_id not in self.current_notes:
self.press_key(key_id)
def on_release(self, event):
key_id = self.find_key(event.x, event.y)
if key_id and key_id in self.current_notes:
self.release_key(key_id)
def find_key(self, x, y):
for key_id in self.black_key_ids:
coords = self.canvas.coords(key_id)
if self.point_in_rect(x, y, coords):
return key_id
for key_id in self.white_key_ids:
coords = self.canvas.coords(key_id)
if self.point_in_rect(x, y, coords):
return key_id
return None
@staticmethod
def point_in_rect(x, y, rect):
x1, y1, x2, y2 = rect
return x1 <= x <= x2 and y1 <= y <= y2
def press_key(self, key_id):
if key_id in self.white_key_ids:
self.canvas.itemconfig(key_id, fill="#ffdead")
else:
self.canvas.itemconfig(key_id, fill="#555555")
midi_note = self.key_id_to_note[key_id]
player.note_on(midi_note, 127)
self.current_notes.add(key_id)
def release_key(self, key_id):
if key_id in self.white_key_ids:
self.canvas.itemconfig(key_id, fill="white")
else:
self.canvas.itemconfig(key_id, fill="black")
midi_note = self.key_id_to_note[key_id]
player.note_off(midi_note, 127)
self.current_notes.discard(key_id)
def change_instrument(self):
inst_name = self.instrument_var.get()
inst_num = INSTRUMENTS.get(inst_name, 0)
player.set_instrument(inst_num)
# --- タブ2: コード進行 ---
class ChordProgressionTab(ttk.Frame):
def __init__(self, master):
super().__init__(master)
self.key_var = tk.StringVar(value="C")
self.style_var = tk.StringVar(value="ポップ")
self.mode_var = tk.StringVar(value="スタイル固定")
self.bpm_var = tk.IntVar(value=90)
self.result_var = tk.StringVar()
self.create_widgets()
for i in range(7):
self.rowconfigure(i, weight=1)
for j in range(2):
self.columnconfigure(j, weight=1)
def create_widgets(self):
pad = {"padx":5, "pady":5}
ttk.Label(self, text="キー").grid(row=0, column=0, sticky="w", **pad)
ttk.Combobox(self, textvariable=self.key_var, values=list(DIATONIC_CHORDS.keys()), state="readonly").grid(row=0, column=1, sticky="ew", **pad)
ttk.Label(self, text="スタイル(進行パターン)").grid(row=1, column=0, sticky="w", **pad)
ttk.Combobox(self, textvariable=self.style_var, values=list(PROGRESSIONS.keys()), state="readonly").grid(row=1, column=1, sticky="ew", **pad)
ttk.Label(self, text="モード").grid(row=2, column=0, sticky="w", **pad)
ttk.Combobox(self, textvariable=self.mode_var, values=["スタイル固定", "ランダム", "拡張コード"], state="readonly").grid(row=2, column=1, sticky="ew", **pad)
ttk.Label(self, text="BPM").grid(row=3, column=0, sticky="w", **pad)
ttk.Entry(self, textvariable=self.bpm_var).grid(row=3, column=1, sticky="ew", **pad)
ttk.Button(self, text="コード進行生成", command=self.generate_progression).grid(row=4, column=0, columnspan=2, sticky="ew", pady=10, padx=5)
ttk.Label(self, textvariable=self.result_var, font=("Arial", 14), anchor="center").grid(row=5, column=0, columnspan=2, sticky="ew", pady=10, padx=5)
ttk.Button(self, text="再生", command=self.play_progression).grid(row=6, column=0, sticky="ew", padx=5, pady=5)
ttk.Button(self, text="MIDI保存", command=self.save_progression).grid(row=6, column=1, sticky="ew", padx=5, pady=5)
def generate_progression(self):
key = self.key_var.get()
style = self.style_var.get()
mode = self.mode_var.get()
chords = generate_progression(key, style, mode)
if chords:
self.result_var.set(" → ".join(chords))
else:
self.result_var.set("")
def play_progression(self):
chords_text = self.result_var.get()
if not chords_text:
messagebox.showwarning("警告", "コード進行を生成してください。")
return
chords = chords_text.split(" → ")
bpm = self.bpm_var.get()
pattern = "シンプル"
play_chords(chords, bpm, pattern)
def save_progression(self):
chords_text = self.result_var.get()
if not chords_text:
messagebox.showwarning("警告", "コード進行を生成してください。")
return
chords = chords_text.split(" → ")
bpm = self.bpm_var.get()
save_midi(chords, bpm)
# --- メインアプリ ---
class MainApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("音楽アプリ 統合版")
self.geometry("600x500")
self.protocol("WM_DELETE_WINDOW", self.on_close)
self.notebook = ttk.Notebook(self)
self.notebook.pack(fill="both", expand=True)
global piano_tab
piano_tab = PianoTab(self.notebook)
self.notebook.add(piano_tab, text="リアルピアノ")
chord_tab = ChordProgressionTab(self.notebook)
self.notebook.add(chord_tab, text="コード進行ジェネレーター")
def on_close(self):
player.close()
pygame.midi.quit()
self.destroy()
if __name__ == "__main__":
app = MainApp()
app.mainloop()
4. 使い方ガイド
- アプリを起動すると、「リアルピアノ」「コード進行ジェネレーター」タブが表示されます。
- ピアノタブで鍵盤をクリックすると音が鳴ります。音色も選べます。
- コード進行タブではキー、スタイル、モード、BPMを設定し、「コード進行生成」をクリック。
- 生成されたコード進行が表示され、「再生」で演奏、「MIDI保存」でファイル出力が可能です。
5. exeファイルのダウンロード
下記リンクよりWindows用の実行ファイルをダウンロードしてご利用ください。
6. まとめ&今後の展望
Pythonで音楽アプリを作る楽しさを体験できる本アプリ。
今後は、もっと多彩な音色対応やコード解析機能、UIの強化を目指しています。
バグ等ありましたらご連絡ください。