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()
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')
3. 下载