【Pythonで作る】リアルタイムピアノ&コード進行ジェネレーター 完全解説

こんにちは!今回はPythonを使って作成した音楽アプリを紹介します。
リアルタイムで演奏できるピアノと、音楽理論を活かしたコード進行ジェネレーターを搭載。
さらに、作ったコード進行はMIDIファイルとして保存も可能です。


目次

  1. アプリの概要
  2. 主な機能紹介
  3. Python全コード掲載
  4. 使い方ガイド
  5. exeファイルのダウンロード
  6. まとめ&今後の展望

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. 使い方ガイド

  1. アプリを起動すると、「リアルピアノ」「コード進行ジェネレーター」タブが表示されます。
  2. ピアノタブで鍵盤をクリックすると音が鳴ります。音色も選べます。
  3. コード進行タブではキー、スタイル、モード、BPMを設定し、「コード進行生成」をクリック。
  4. 生成されたコード進行が表示され、「再生」で演奏、「MIDI保存」でファイル出力が可能です。

5. exeファイルのダウンロード

下記リンクよりWindows用の実行ファイルをダウンロードしてご利用ください。


6. まとめ&今後の展望

Pythonで音楽アプリを作る楽しさを体験できる本アプリ。
今後は、もっと多彩な音色対応やコード解析機能、UIの強化を目指しています。

バグ等ありましたらご連絡ください。

タイトルとURLをコピーしました