x01.piano: 钢琴练习

钢琴练习,只需键盘即可,但添加音阶(scale),和弦(chord),和弦进行(chord progression)的处理,五线谱(score)的显示,似乎也还不错。本以为 tkinter 的界面编程偏弱,但通过学习,发现其快速强大,丝毫不弱!

1. 效果图

        

2. 代码

import os
import sys
import tkinter as tk
from tkinter import colorchooser, filedialog, messagebox

# 为引用 utils,在 site-packages 目录下新建 mypath.pth 文件,
# 添加所需导入模块的目录路径, 如 ‘x01.lab/py/’ 所在路径。
import utils

sys.path.append(utils.R.CurrentDir)

from piano.core import PianoFrame, R

class MainWindow(tk.Tk):
    Title = 'x01.piano'
    
    def __init__(self):
        super().__init__()
        self.title(self.Title)
        utils.R.win_center(self,w=R.WinWidth, h=R.WinHeight)
        self.resizable(False, False)

        self.menu = tk.Menu(self)
        utils.R.generate_menus(self,['file', 'help'])
        self.configure(menu=self.menu)

        self.piano_frame = PianoFrame(self, width=R.WinWidth, height=R.WinHeight)
        self.piano_frame.pack()

    def file_quit(self):
        self.destroy()

   

if __name__ == "__main__": 
    win = MainWindow()
    win.mainloop()
main.py
import json
import os
import sys
import time
import tkinter as tk
import tkinter.ttk as ttk
from collections import OrderedDict
from tkinter import colorchooser, filedialog, messagebox
import itertools
from functools import partial 

import simpleaudio

import utils
from _thread import start_new_thread


class R:
    WinWidth = 560
    ModeHeight = 50
    ScoreHeight = 110
    ControlHeight = 100
    KeyHeight = 160
    WinHeight = ModeHeight + ControlHeight + KeyHeight + ScoreHeight

    Choices = ['Scales', 'Chords', 'Chord Progressions']
    ImagePath = os.path.join(utils.R.CurrentDir, 'piano/img/')
    SoundsPath = os.path.join(utils.R.CurrentDir, 'piano/sounds/')
    JsonPath = os.path.join(utils.R.CurrentDir, 'piano/json/')

    WhiteKeyNames = ['C1','D1', 'E1', 'F1', 'G1','A1', 'B1', 'C2','D2', 'E2', 'F2', 'G2','A2', 'B2']
    BlackKeyNames = ['C#1', 'D#1', 'F#1', 'G#1', 'A#1', 'C#2', 'D#2', 'F#2', 'G#2', 'A#2']
    WhiteKeyXCoordinates = [0,40, 80,120, 160, 200, 240,280, 320, 360, 400, 440, 480,520]
    BlackKeyXCoordinates = [30,70,150,190, 230, 310, 350, 430,470, 510]

    AllKeys= ['C1','C#1','D1','D#1','E1','F1','F#1','G1','G#1','A1', 
            'A#1','B1', 'C2','C#2','D2','D#2','E2','F2','F#2','G2',
            'G#2','A2','A#2','B2']
    Keys = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
    
    Romans = { 'I':0, 'II': 2, 'III':4, 'IV':5, 'V': 7, 'VI':9, 'VII': 11,
        'i':0, 'ii': 2, 'iii':4, 'iv':5, 'v': 7, 'vi':9, 'vii': 11}    


class PianoFrame(tk.Frame):
    def __init__(self, master=None, cnf=None, **kw):
        super().__init__(master=master, cnf=cnf, **kw)
        self.master = master 
        self.images = {
            'black_key': tk.PhotoImage(file=os.path.join(R.ImagePath, 'black_key.gif')),
            'white_key': tk.PhotoImage(file=os.path.join(R.ImagePath, 'white_key.gif')),
            'black_key_pressed': tk.PhotoImage(file=os.path.join(R.ImagePath, 'black_key_pressed.gif')),
            'white_key_pressed': tk.PhotoImage(file=os.path.join(R.ImagePath, 'white_key_pressed.gif'))
        }
        self.player = AudioPlayer()
        self.keys = []
        self.highlight_keys = []
        self.progression_buttons = []

        self.scales = self.load_json_files(filename=os.path.join(R.JsonPath, 'scales.json'))
        self.chords = self.load_json_files(filename=os.path.join(R.JsonPath, 'chords.json'))
        self.progressions = self.load_json_files(filename=os.path.join(R.JsonPath, 'progressions.json'))
        
        self.create_mode_frame()
        self.create_score_frame()
        self.create_control_frame()
        self.create_key_frame()
        
        self.create_chords_frame()
        self.create_progression_frame()
        self.create_scales_frame()

        self.find_scale()


    def create_mode_frame(self):
        frame = tk.Frame(self, width=R.WinWidth, height=R.ModeHeight)
        frame.grid_propagate(False)
        mode_combox = ttk.Combobox(frame, values=R.Choices)
        mode_combox.bind('<<ComboboxSelected>>', self.mode_selected)
        mode_combox.current(0)
        mode_combox.grid()
        frame.grid(row=0,column=0)
        self.mode_combox = mode_combox

    def mode_selected(self, e=None):
        mode = self.mode_combox.get()
        if mode == 'Scales':
            self.show_scales_frame()
        elif mode == 'Chords':
            self.show_chords_frame()
        elif mode == 'Chord Progressions':
            self.show_progression_frame()

    def show_scales_frame(self):
        self.chords_frame.grid_remove()
        self.progression_frame.grid_remove()
        self.scales_frame.grid()

    def show_chords_frame(self):
        self.scales_frame.grid_remove()
        self.progression_frame.grid_remove()
        self.chords_frame.grid()

    def show_progression_frame(self):
        self.scales_frame.grid_remove()
        self.chords_frame.grid_remove()
        self.progression_frame.grid()

    def create_score_frame(self): 
        frame = tk.Frame(self, width=R.WinWidth, height=R.ScoreHeight)
        frame.grid_propagate(False)
        frame.grid(row=1,column=0)
        self.score_frame = frame 
        self.score_maker = ScoreMaker(self.score_frame)

    def create_control_frame(self): 
        frame = tk.Frame(self, width=R.WinWidth, height=R.ControlHeight)
        frame.grid_propagate(False)
        frame.grid(row=2,column=0)
        self.control_frame = frame

    def create_key_frame(self):
        frame = tk.Frame(self, width=R.WinWidth, height=R.KeyHeight, background='LavenderBlush2')
        frame.grid_propagate(False)
        tk.Label(frame, text='placeholder for key frame').grid()
        frame.grid(row=4,column=0, sticky='nsew')
        self.key_frame = frame 
        for i, key in enumerate(R.WhiteKeyNames):
            x = R.WhiteKeyXCoordinates[i]
            self.create_key(self.images['white_key'], key, x)
        for i, key in enumerate(R.BlackKeyNames):
            x = R.BlackKeyXCoordinates[i]
            self.create_key(self.images['black_key'], key, x)

    def create_key(self, image, key, x):
        label = tk.Label(self.key_frame, image=image, border=0)
        label.place(x=x, y=0)
        label.name = key 
        label.bind('<Button-1>', self.key_pressed)
        label.bind('<ButtonRelease-1>', self.key_released)
        self.keys.append(label)
        return label 

    def key_pressed(self, e=None):
        self.player.play_note(e.widget.name)
        if len(e.widget.name) == 2:
            img = self.images['white_key_pressed']        
        elif len(e.widget.name) == 3:
            img = self.images['black_key_pressed']
        e.widget.config(image=img)

    def key_released(self, e=None): 
        if len(e.widget.name) == 2:
            img = self.images['white_key']        
        elif len(e.widget.name) == 3:
            img = self.images['black_key']
        e.widget.config(image=img)

    def create_scales_frame(self): 
        frame = tk.Frame(self.control_frame, width=R.WinWidth, height=R.ControlHeight)
        tk.Label(frame, text='Select scale').grid(row=0,column=1,stick='w',padx=10,pady=1)
        scale_combox = ttk.Combobox(frame, values=[k for k in self.scales.keys()])
        scale_combox.current(0)
        scale_combox.bind('<<ComboboxSelected>>', self.scale_changed)
        scale_combox.grid(row=1, column=1, sticky='e', padx=10, pady=10)
        tk.Label(frame, text='In the key of').grid(row=0, column=2, sticky='w', padx=10, pady=1)
        scale_key_combox = ttk.Combobox(frame, values=[k for k in R.Keys])
        scale_key_combox.current(0)
        scale_key_combox.bind('<<ComboboxSelected>>', self.scale_key_changed)
        scale_key_combox.grid(row=1, column=2, sticky='e', padx=10, pady=10)
        frame.grid(row=1,column=0, sticky='nsew')
        self.scales_frame = frame 
        self.scale_combox = scale_combox
        self.scale_key_combox = scale_key_combox

    def scale_changed(self, e=None):
        self.remove_all_key_highlight()
        self.find_scale(e)

    def scale_key_changed(self, e=None):
        self.remove_all_key_highlight()
        self.find_scale(e)

    def remove_all_key_highlight(self):
        for key in self.highlight_keys:
            self.remove_key_highlight(key)
        self.highlight_keys = []

    def remove_key_highlight(self, key):
        if len(key) == 2:
            img = self.images['white_key']
        elif len(key) == 3:
            img = self.images['black_key']
        for w in self.keys:
            if w.name == key:
                w.configure(image=img)

    def find_scale(self, e=None):
        self.selected_scale = self.scale_combox.get()
        self.scale_selected_key = self.scale_key_combox.get()
        index = R.Keys.index(self.scale_selected_key)
        self.highlight_keys = [R.AllKeys[i+index] for i in self.scales[self.selected_scale]]
        self.highlight_list_of_keys(self.highlight_keys)
        self.player.play_scale_in_new_thread(self.highlight_keys)
        self.score_maker.draw_notes(self.highlight_keys)

    def highlight_list_of_keys(self, key_names):
        for key in key_names:
            self.highlight_key(key)

    def highlight_key(self, key):
        if len(key) == 2:
            img = self.images['white_key_pressed']
        elif len(key) == 3:
            img = self.images['black_key_pressed']
        for w in self.keys:
            if w.name == key:
                w.configure(image=img)


    def create_chords_frame(self): 
        frame = tk.Frame(self.control_frame, width=R.WinWidth, height=R.ControlHeight)
        frame.grid_propagate(False)
        frame.grid(row=1,column=0, sticky='nsew')
        tk.Label(frame, text='Selected Chord').grid(row=0,column=1, sticky='w', padx=10, pady=1)
        chords_combox = ttk.Combobox(frame, values=[k for k in self.chords.keys()])
        chords_combox.current(0)
        chords_combox.bind('<<ComboboxSelected>>', self.chord_changed)
        chords_combox.grid(row=1, column=1, sticky='e', padx=10, pady=10)
        tk.Label(frame, text='in the key of').grid(row=0, column=2, sticky='w', padx=10, pady=1)
        chords_key_combox = ttk.Combobox(frame, values=[k for k in R.Keys])
        chords_key_combox.current(0)
        chords_key_combox.bind('<<ComboboxSelected>>', self.chords_key_changed)
        chords_key_combox.grid(row=1, column=2, sticky='e', padx=10, pady=10)
        self.chords_combox = chords_combox
        self.chords_key_combox = chords_key_combox
        self.chords_frame = frame 

    def chord_changed(self, e=None):
        self.remove_all_key_highlight()
        self.find_chord(e)

    def chords_key_changed(self, e=None):
        self.remove_all_key_highlight()
        self.find_chord(e)

    def find_chord(self, e=None):
        self.selected_chord = self.chords_combox.get()
        self.chords_selected_key = self.chords_key_combox.get()
        index = R.Keys.index(self.chords_selected_key)
        self.highlight_keys = [R.AllKeys[i+index] for i in self.chords[self.selected_chord]]
        self.score_maker.draw_chord(self.highlight_keys)
        self.highlight_list_of_keys(self.highlight_keys)
        self.player.play_chord_in_new_thread(self.highlight_keys)

    def create_progression_frame(self): 
        frame = tk.Frame(self.control_frame, width=R.WinWidth, height=R.ControlHeight)
        frame.grid_propagate(False)
        frame.grid(row=1,column=0, sticky='nsew')
        tk.Label(frame, text='Select Scales').grid(row=0, column=1, sticky='w', padx=10, pady=1)
        tk.Label(frame, text='Select Progression').grid(row=0,column=2,sticky='w', padx=10, pady=1)
        tk.Label(frame, text='in the key of').grid(row=0, column=3, sticky='w', padx=10, pady=1)
        progression_scale_combox = ttk.Combobox(frame, values=[k for k in self.progressions.keys()], width=18)
        progression_scale_combox.bind('<<ComboboxSelected>>', self.progression_scale_changed)
        progression_scale_combox.current(0)
        progression_scale_combox.grid(row=1, column=1, sticky='w', padx=10, pady=5)
        progression_combbox = ttk.Combobox(frame, values=[k for k in self.progressions['Major'].keys()], width=18)
        progression_combbox.bind('<<ComboboxSelected>>', self.progression_changed)
        progression_combbox.current(0)
        progression_combbox.grid(row=1, column=2, sticky='w', padx=10, pady=5)
        progression_key_combox = ttk.Combobox(frame, values=R.Keys, width=18)
        progression_key_combox.current(0)
        progression_key_combox.bind('<<ComboboxSelected>>', self.progression_key_changed)
        progression_key_combox.grid(row=1, column=3, sticky='w', padx=10, pady=5)
        self.progression_combbox = progression_combbox
        self.progression_key_combox = progression_key_combox
        self.progression_scale_combox = progression_scale_combox
        self.progression_frame = frame 

    def progression_changed(self, e=None): 
        self.show_progression_buttons()

    def progression_key_changed(self, e=None):
        self.show_progression_buttons()

    def progression_scale_changed(self, e=None): 
        selected_progression_scale = self.progression_scale_combox.get()
        progressions = [k for k in self.progressions[selected_progression_scale].keys()]
        self.progression_combbox['values'] = progressions
        self.progression_combbox.current(0)
        self.show_progression_buttons()

    def show_progression_buttons(self):
        self.destory_current_progression_buttons()
        selected_progression_scale = self.progression_scale_combox.get()
        selected_progression = self.progression_combbox.get().split('-')
        self.progression_buttons = []
        for i in range(len(selected_progression)):
            self.progression_buttons.append(tk.Button(self.progression_frame, text=selected_progression[i],
                command=partial(self.progression_button_clicked, i)))
            sticky = ('w' if i == 0 else 'e')
            col = (i if i>1 else 1)
            self.progression_buttons[i].grid(row=2, column=col, sticky=sticky, padx=5)

    def progression_button_clicked(self, i):
        self.remove_all_key_highlight()
        selected_progression = self.progression_combbox.get().split('-')[i]
        if any(x.isupper() for x in selected_progression):
            selected_chord = 'Major'
        else:
            selected_chord = 'Minor'
        key_offset = R.Romans[selected_progression]
        selected_key = self.progression_key_combox.get()
        index = (R.Keys.index(selected_key) + key_offset) % 12
        self.highlight_keys = [R.AllKeys[j + index] for j in self.chords[selected_chord]]
        self.score_maker.draw_chord(self.highlight_keys)
        self.highlight_list_of_keys(self.highlight_keys)
        self.player.play_chord_in_new_thread(self.highlight_keys)

    def destory_current_progression_buttons(self): 
        for b in self.progression_buttons:
            b.destroy()

    def load_json_files(self, filename):
        with open(filename, 'r') as f:
            data = json.load(f, object_pairs_hook=OrderedDict)
            return data 

class AudioPlayer:
    def play_note(self, note_name):
        wave = simpleaudio.WaveObject.from_wave_file(os.path.join(R.SoundsPath, note_name + '.wav'))
        wave.play()

    def play_scale(self, scale):
        for note in scale:
            self.play_note(note)
            time.sleep(0.5)

    def play_scale_in_new_thread(self, scale):
        start_new_thread(self.play_scale, (scale, ))

    def play_chord(self, chord):
        for note in chord:
            self.play_note(note)

    def play_chord_in_new_thread(self, chord):
        start_new_thread(self.play_chord, (chord, ))

class ScoreMaker(tk.Frame):
    def  __init__(self, master=None):
        super().__init__(master=master)
        self.canvas = tk.Canvas(self.master, width=500, height=R.ScoreHeight)
        self.canvas.grid(row=0, column=1)
        self.master = master 
        master.update_idletasks()
        self.canvas_width = R.WinWidth
        self.sharp_image = tk.PhotoImage(file=os.path.join(R.ImagePath, 'sharp.gif'))
        self.treble_clef_image = tk.PhotoImage(file=os.path.join(R.ImagePath, 'treble_clef.gif'))
        self.x_counter = itertools.count(start=50, step=30)

    def _clean_score_sheet(self):
        self.x_counter = itertools.count(start=50, step=30)
        self.canvas.delete('all')

    def _create_treble_staff(self):
        self._draw_five_lines()
        self.canvas.create_image(10,20,image=self.treble_clef_image, anchor='nw')

    def draw_chord(self, chord):
        self._clean_score_sheet()
        self._create_treble_staff()
        for note in chord:
            self._draw_single_note(note, is_in_chord=True)

    def _draw_five_lines(self):
        w = self.canvas_width
        for i in range(5):
            self.canvas.create_line(0,40+i*10, w, 40+i*10, fill='#555')

    def draw_notes(self, notes):
        self._clean_score_sheet()
        self._create_treble_staff()
        for note in notes:
            self._draw_single_note(note)

    def _draw_single_note(self, note, is_in_chord=False):
        is_sharp = '#' in note 
        note = note.replace('#', '')
        radius = 9
        if is_in_chord:
            x = 75
        else:
            x = next(self.x_counter)
        i = R.WhiteKeyNames.index(note)
        y = 85 - 5*i 
        self.canvas.create_oval(x,y,x+radius, y+radius, fill='#555')
        if is_sharp:
            self.canvas.create_image(x-10, y, image=self.sharp_image, anchor='nw')
        if note == 'C1':
            self.canvas.create_line(x-5, 90, x+15, 90, fill='#555')
        elif note == 'G2':
            self.canvas.create_line(x-5, 35, x+15, 35, fill='#555')
        elif note == 'A2':
            self.canvas.create_line(x-5, 35, x+15, 35, fill='#555')
        elif note == 'B2':
            self.canvas.create_line(x-5, 35, x+15, 35, fill='#555')
            self.canvas.create_line(x-5, 25, x+15, 25, fill='#555')
core.py

3. 下载

x01.lab/py/piano

原文地址:https://www.cnblogs.com/china_x01/p/12986099.html