def __init__(self, notebook: Notebook, exercise: Exercise): # some control variables self.stopping = False self.play_next = False self.repeat_once = False self.name = exercise.name self.config = config = exercise.config self._data_path = exercise.data_path self._include_path = exercise.include_path self.tab = Frame(notebook) self.tab.rowconfigure(1, weight=1) self.tab.columnconfigure(3, weight=1) notebook.append({'name': self.name, 'widget': self.tab}) # bpm selector self.bpm_label = Label(self.tab, text="bpm:") self.bpm_label.grid(column=2, row=0, sticky=tk.N + tk.W) self.bpm = Scale(self.tab, from_=80, to=160, tickinterval=10, showvalue=0, length=300, resolution=10, default=140, orient=tk.HORIZONTAL) self.bpm.grid(column=3, row=0, sticky=tk.W + tk.N) if 'tempo' not in config: self.bpm.disable() # pitch selector self.pitch_range = Listbox(self.tab, width=3, values=PITCH_LIST) self.pitch_range.grid(row=1, column=1, sticky=tk.N + tk.S + tk.W) self.pitch_range.set({pitch: True for pitch in PITCH_LIST[7:14]}) # controls self.control_frame = Frame(self.tab) self.control_frame.grid(column=2, row=2, columnspan=2, sticky=tk.W + tk.N) self._create_controls(self.control_frame, config) # sheet display self._image_cache = {} self.sheet = tk.Canvas(self.tab.raw, bd=0, highlightthickness=0) self.sheet.bind("<Configure>", lambda e: asyncio.ensure_future(self._resize_sheet(e))) self.sheet.grid(column=2, row=1, columnspan=2, sticky=tk.NSEW) self.sheet.bind("<Button-1>", lambda e: self._set_repeat_once()) asyncio.ensure_future(self._update_sheet())
def _create_controls(self, parent, config): self.velocity_label = Label(parent, text="relative velocity:") self.velocity_label.grid(column=0, row=0, sticky=tk.N + tk.E) self.velocity = Spinbox(parent, width=3, from_=-50, to=50, default=0, increment=1) self.velocity.grid(column=1, row=0, sticky=tk.W + tk.N) self.sound = OptionMenu( parent, default='Mi', option_list=SOUND_LIST, command=lambda _: asyncio.ensure_future(self._update_sheet())) self.sound.grid(column=2, row=0, sticky=tk.W + tk.N) if 'sound' not in config: self.sound.disable() self.key = OptionMenu( parent, option_list=PITCH_LIST, default='c', command=lambda _: asyncio.ensure_future(self._on_pitch_change())) self.key.grid(column=3, row=0, sticky=tk.W + tk.N) if 'key' in config: self.key.set(config['key']) else: self.key.disable() self.random = Checkbutton(parent, text="random") self.random.grid(column=4, row=0, sticky=tk.W + tk.N) self.autonext = Checkbutton(parent, text="autonext") self.autonext.grid(column=5, row=0, sticky=tk.W + tk.N) self.b_repeat = Button(parent, text="Repeat once", command=self._set_repeat_once) self.b_repeat.grid(column=6, row=0, sticky=tk.W + tk.N) self.b_next_ = Button( parent, text="Next", command=lambda: asyncio.ensure_future(self._next())) self.b_next_.grid(column=7, row=0, sticky=tk.W + tk.N) self.b_play = Button( parent, text="Play", command=lambda: asyncio.ensure_future(self.play())) self.b_play.grid(column=8, row=0, sticky=tk.W + tk.N)
class ExerciseTab: """Tab containing everything for one exersise.""" def __init__(self, notebook: Notebook, exercise: Exercise): # some control variables self.stopping = False self.play_next = False self.repeat_once = False self.name = exercise.name self.config = config = exercise.config self._data_path = exercise.data_path self._include_path = exercise.include_path self.tab = Frame(notebook) self.tab.rowconfigure(1, weight=1) self.tab.columnconfigure(3, weight=1) notebook.append({'name': self.name, 'widget': self.tab}) # bpm selector self.bpm_label = Label(self.tab, text="bpm:") self.bpm_label.grid(column=2, row=0, sticky=tk.N + tk.W) self.bpm = Scale(self.tab, from_=80, to=160, tickinterval=10, showvalue=0, length=300, resolution=10, default=140, orient=tk.HORIZONTAL) self.bpm.grid(column=3, row=0, sticky=tk.W + tk.N) if 'tempo' not in config: self.bpm.disable() # pitch selector self.pitch_range = Listbox(self.tab, width=3, values=PITCH_LIST) self.pitch_range.grid(row=1, column=1, sticky=tk.N + tk.S + tk.W) self.pitch_range.set({pitch: True for pitch in PITCH_LIST[7:14]}) # controls self.control_frame = Frame(self.tab) self.control_frame.grid(column=2, row=2, columnspan=2, sticky=tk.W + tk.N) self._create_controls(self.control_frame, config) # sheet display self._image_cache = {} self.sheet = tk.Canvas(self.tab.raw, bd=0, highlightthickness=0) self.sheet.bind("<Configure>", lambda e: asyncio.ensure_future(self._resize_sheet(e))) self.sheet.grid(column=2, row=1, columnspan=2, sticky=tk.NSEW) self.sheet.bind("<Button-1>", lambda e: self._set_repeat_once()) asyncio.ensure_future(self._update_sheet()) def _create_controls(self, parent, config): self.velocity_label = Label(parent, text="relative velocity:") self.velocity_label.grid(column=0, row=0, sticky=tk.N + tk.E) self.velocity = Spinbox(parent, width=3, from_=-50, to=50, default=0, increment=1) self.velocity.grid(column=1, row=0, sticky=tk.W + tk.N) self.sound = OptionMenu( parent, default='Mi', option_list=SOUND_LIST, command=lambda _: asyncio.ensure_future(self._update_sheet())) self.sound.grid(column=2, row=0, sticky=tk.W + tk.N) if 'sound' not in config: self.sound.disable() self.key = OptionMenu( parent, option_list=PITCH_LIST, default='c', command=lambda _: asyncio.ensure_future(self._on_pitch_change())) self.key.grid(column=3, row=0, sticky=tk.W + tk.N) if 'key' in config: self.key.set(config['key']) else: self.key.disable() self.random = Checkbutton(parent, text="random") self.random.grid(column=4, row=0, sticky=tk.W + tk.N) self.autonext = Checkbutton(parent, text="autonext") self.autonext.grid(column=5, row=0, sticky=tk.W + tk.N) self.b_repeat = Button(parent, text="Repeat once", command=self._set_repeat_once) self.b_repeat.grid(column=6, row=0, sticky=tk.W + tk.N) self.b_next_ = Button( parent, text="Next", command=lambda: asyncio.ensure_future(self._next())) self.b_next_.grid(column=7, row=0, sticky=tk.W + tk.N) self.b_play = Button( parent, text="Play", command=lambda: asyncio.ensure_future(self.play())) self.b_play.grid(column=8, row=0, sticky=tk.W + tk.N) def save_state(self): """Return exercise state.""" data = {} data['pitch_range'] = self.pitch_range.get() data['bpm'] = self.bpm.get() data['velocity'] = self.velocity.get() data['sound'] = self.sound.get() data['autonext'] = self.autonext.get() data['random'] = self.random.get() return data def restore_state(self, data): """Restore saved settings.""" if 'pitch_range' in data: self.pitch_range.set(data['pitch_range']) if 'bpm' in data: self.bpm.set(data['bpm']) if 'velocity' in data: self.velocity.set(data['velocity']) if 'sound' in data: self.sound.set(data['sound']) if 'autonext' in data: self.autonext.set(data['autonext']) if 'random' in data: self.random.set(data['random']) def _get_interface(self): """Return exercise interface.""" return Exercise(self._data_path, self._include_path, name=self.name, pitch=self.key.get(), bpm=self.bpm.get(), sound=self.sound.get(), velocity=self.velocity.get()) async def clear_cache(self): """Remove all compiled files.""" # remove files for file_ in chain(self._data_path.glob("{}-*.midi".format(self.name)), self._data_path.glob("{}-*.png".format(self.name)), self._data_path.glob("{}-*.pdf".format(self.name))): file_.unlink() self._image_cache = {} await self._update_sheet() async def _update_sheet(self): """Display relevant sheet.""" # pylint: disable=invalid-name # type declaration Size = namedtuple('Size', ['width', 'height']) size = Size(self.sheet.winfo_width(), self.sheet.winfo_height()) await self._resize_sheet(size) async def _resize_sheet(self, event): """Resize sheets to screen size.""" self.sheet.delete("left") left = self._get_interface() # make sure pages are compiled await get_file(left) left_path = await get_single_sheet(self._image_cache, left, event.width, event.height) self.sheet.create_image(0, 0, image=self._image_cache[left_path]['image'], anchor=tk.NW, tags="left") async def _on_pitch_change(self): """New pitch was picked by user or app.""" asyncio.ensure_future(self._update_sheet()) if is_playing(): self.play_next = True await stop() else: await self.play() def _set_repeat_once(self, _=None): """Repeat this midi once, then continue.""" self.repeat_once = True async def _next_sound(self): if 'sound' in self.config: current_sound = self.sound.get() sound_position = SOUND_LIST.index(current_sound) if sound_position < len(SOUND_LIST) - 1: self.sound.set(SOUND_LIST[sound_position + 1]) else: self.sound.set(SOUND_LIST[0]) asyncio.ensure_future(self._update_sheet()) async def _next(self): """Pick next exercise configuration.""" if 'key' not in self.config: await self._next_sound() return current_pitch = new_pitch = self.key.get() pitch_position = PITCH_LIST.index(current_pitch) pitch_range = self.pitch_range.get() pitch_selection = [ pitch for pitch in pitch_range if pitch_range[pitch] ] if not pitch_selection: return if self.random.get(): while new_pitch == current_pitch: new_pitch = choice(pitch_selection) else: while pitch_selection: pitch_position += 1 if pitch_position >= len(PITCH_LIST): pitch_position = 0 await self._next_sound() if PITCH_LIST[pitch_position] in pitch_selection: new_pitch = PITCH_LIST[pitch_position] break self.key.set(new_pitch) await self._on_pitch_change() async def play(self): """Play midi file.""" midi = await get_file(self._get_interface(), FileType.midi) if is_playing(): self.stopping = True playing = await play_or_stop(midi, self._on_midi_stop) if playing: self.b_play.set_text("Stop") else: self.b_play.set_text("Play") async def _on_midi_stop(self): """Handle end of midi playback.""" if self.stopping: self.play_next = False self.stopping = False self.b_play.set_text("Play") return if self.play_next or self.autonext.get() == 1: self.play_next = False if not self.repeat_once: await self._next() else: self.repeat_once = False await self.play() return self.b_play.set_text("Play") def export(self): """Export compiled data.""" return self._get_interface()
def create_widgets(self): """Put some stuff up to look at.""" self.window = Frame(self.root) self.window.rowconfigure(0, weight=1) self.window.columnconfigure(2, weight=1) self.window.grid(column=0, row=0, sticky=tk.N + tk.S + tk.E + tk.W) self.top = self.window.winfo_toplevel() self.top.rowconfigure(0, weight=1) self.top.columnconfigure(0, weight=1) self.top.title("VoiceTrainer") self.top.protocol("WM_DELETE_WINDOW", lambda: asyncio.ensure_future(self.quit())) # self.top menu self.menubar = tk.Menu(self.top) self.top['menu'] = self.menubar self.file_menu = tk.Menu(self.menubar) self.menubar.add_cascade(label='File', menu=self.file_menu) self.file_menu.add_command( label='Select port', command=lambda: asyncio.ensure_future(self.select_port())) self.file_menu.add_separator() self.file_menu.add_command( label='Export midi', command=lambda: asyncio.ensure_future(self.export(FileType.midi))) self.file_menu.add_command( label='Export png', command=lambda: asyncio.ensure_future(self.export(FileType.png))) self.file_menu.add_command( label='Export pdf', command=lambda: asyncio.ensure_future(self.export(FileType.pdf))) self.file_menu.add_command( label='Export lilypond', command=lambda: asyncio.ensure_future(self.export(FileType.lily))) self.file_menu.add_separator() self.file_menu.add_command(label='Save state', command=self.save_state) self.file_menu.add_command( label='Clear cache', command=lambda: asyncio.ensure_future(self.clear_cache())) self.file_menu.add_command( label='Quit', command=lambda: asyncio.ensure_future(self.quit())) self.notebook = Notebook(self.window) self.notebook.grid(column=0, row=0, columnspan=3, sticky=tk.N + tk.S + tk.E + tk.W) self.notebook.rowconfigure(0, weight=1) self.notebook.columnconfigure(0, weight=1) self.separators = [] self.statusbar = Frame(self.window) self.statusbar.grid(row=1, column=0, sticky=tk.N + tk.W) self.compiler_label = Label(self.statusbar, text="Compiler:") self.compiler_label.grid(row=0, column=0, sticky=tk.N + tk.E) self.progress = ttk.Progressbar(self.statusbar.raw, mode='indeterminate', orient=tk.HORIZONTAL) self.progress.grid(row=0, column=1, sticky=tk.N + tk.W) sep = ttk.Separator(self.statusbar.raw, orient=tk.VERTICAL) sep.grid(row=0, column=2) self.separators.append(sep) self.port_label = Label(self.statusbar, text='pmidi port: None') self.port_label.grid(row=0, column=3, sticky=tk.N + tk.W) sep = ttk.Separator(self.statusbar.raw, orient=tk.VERTICAL) sep.grid(row=0, column=4) self.separators.append(sep) self.msg_button = Button(self.statusbar.raw, text='messages', command=self.display_messages) self.msg_button.grid(row=0, column=5, sticky=tk.N + tk.W) self.resize = ttk.Sizegrip(self.window.raw) self.resize.grid(row=1, column=2, sticky=tk.S + tk.E)
class MainWindow(ExerciseMixin, SongMixin): """Voicetrainer application.""" def __init__(self, root): self.data_path = Path().home().joinpath('.voicetrainer') self.data_path.mkdir(exist_ok=True) self.include_path = self.data_path.joinpath('include') self.include_path.mkdir(exist_ok=True) self.root = root # compiler state self.compiler_count = 0 set_compiler_cb(self.update_compiler) self.messages = [] self.messages_read = 0 self.msg_window = None set_compile_err_cb(self.new_message) set_play_err_cb(self.new_message) # midi state self.port = None self.port_match = 'FLUID' asyncio.ensure_future(self.find_port()) # gui elements storage self.create_widgets() ExerciseMixin.__init__(self) SongMixin.__init__(self) self.restore_state() async def find_port(self): """Find qsynth port.""" try: port_finder = PortFinder(self.port_match) async for port in port_finder: if port is not None: self.port = port set_pmidi_port(port) else: await asyncio.sleep(5) port_finder.match = self.port_match self.port_label.set_text('pmidi port: {}'.format(self.port)) except Exception as err: ErrorDialog(self.root, data="Could not find midi port\n{}".format(str(err))) raise async def select_port(self, pmidi=True): """Change port matching.""" # FIXME: stay away from private mixin properties # pylint: disable=access-member-before-definition,invalid-name # pylint: disable=attribute-defined-outside-init selection_dialog = PortSelection( self.root, data=await list_ports(pmidi=pmidi), current_port=str(self.port_match) if pmidi else str(self._SongMixin__jpmidi_port)) if pmidi: self.port_match = await selection_dialog.await_data() if self.port is not None: # not currently searching for port, start self.port = None self.port_label.set_text('pmidi port: None') await self.find_port() else: self._SongMixin__jpmidi_port = await selection_dialog.await_data() def create_widgets(self): """Put some stuff up to look at.""" self.window = Frame(self.root) self.window.rowconfigure(0, weight=1) self.window.columnconfigure(2, weight=1) self.window.grid(column=0, row=0, sticky=tk.N + tk.S + tk.E + tk.W) self.top = self.window.winfo_toplevel() self.top.rowconfigure(0, weight=1) self.top.columnconfigure(0, weight=1) self.top.title("VoiceTrainer") self.top.protocol("WM_DELETE_WINDOW", lambda: asyncio.ensure_future(self.quit())) # self.top menu self.menubar = tk.Menu(self.top) self.top['menu'] = self.menubar self.file_menu = tk.Menu(self.menubar) self.menubar.add_cascade(label='File', menu=self.file_menu) self.file_menu.add_command( label='Select port', command=lambda: asyncio.ensure_future(self.select_port())) self.file_menu.add_separator() self.file_menu.add_command( label='Export midi', command=lambda: asyncio.ensure_future(self.export(FileType.midi))) self.file_menu.add_command( label='Export png', command=lambda: asyncio.ensure_future(self.export(FileType.png))) self.file_menu.add_command( label='Export pdf', command=lambda: asyncio.ensure_future(self.export(FileType.pdf))) self.file_menu.add_command( label='Export lilypond', command=lambda: asyncio.ensure_future(self.export(FileType.lily))) self.file_menu.add_separator() self.file_menu.add_command(label='Save state', command=self.save_state) self.file_menu.add_command( label='Clear cache', command=lambda: asyncio.ensure_future(self.clear_cache())) self.file_menu.add_command( label='Quit', command=lambda: asyncio.ensure_future(self.quit())) self.notebook = Notebook(self.window) self.notebook.grid(column=0, row=0, columnspan=3, sticky=tk.N + tk.S + tk.E + tk.W) self.notebook.rowconfigure(0, weight=1) self.notebook.columnconfigure(0, weight=1) self.separators = [] self.statusbar = Frame(self.window) self.statusbar.grid(row=1, column=0, sticky=tk.N + tk.W) self.compiler_label = Label(self.statusbar, text="Compiler:") self.compiler_label.grid(row=0, column=0, sticky=tk.N + tk.E) self.progress = ttk.Progressbar(self.statusbar.raw, mode='indeterminate', orient=tk.HORIZONTAL) self.progress.grid(row=0, column=1, sticky=tk.N + tk.W) sep = ttk.Separator(self.statusbar.raw, orient=tk.VERTICAL) sep.grid(row=0, column=2) self.separators.append(sep) self.port_label = Label(self.statusbar, text='pmidi port: None') self.port_label.grid(row=0, column=3, sticky=tk.N + tk.W) sep = ttk.Separator(self.statusbar.raw, orient=tk.VERTICAL) sep.grid(row=0, column=4) self.separators.append(sep) self.msg_button = Button(self.statusbar.raw, text='messages', command=self.display_messages) self.msg_button.grid(row=0, column=5, sticky=tk.N + tk.W) self.resize = ttk.Sizegrip(self.window.raw) self.resize.grid(row=1, column=2, sticky=tk.S + tk.E) async def quit(self): """Confirm exit.""" if self.compiler_count > 0: # ask conformation before quit confirm_exit = OkCancelDialog( self.root, ("Uncompleted background task\n" "An exercise is still being compiled in the " "background. Do you still want to exit? The " "task will be aborted.")) if not await confirm_exit.await_data(): return if is_playing(): await stop() self.close() def close(self): """Exit main application.""" self.save_state() self.progress.stop() self.top.destroy() self.root.close() def save_state(self): """Save settings to json file.""" data = {} data['port_match'] = self.port_match data['exercises'] = ExerciseMixin.save_state(self) data['songs'] = SongMixin.save_state(self) self.data_path.joinpath('state.json').write_text(json.dumps(data)) def restore_state(self, _=None): """Restore saved settings.""" state_file = self.data_path.joinpath('state.json') if not state_file.is_file(): return data = json.loads(state_file.read_text()) if 'port_match' in data: self.port_match = data['port_match'] if 'exercises' in data: ExerciseMixin.restore_state(self, data['exercises']) if 'songs' in data: SongMixin.restore_state(self, data['songs']) def show_messages(self): """Show if there unread messages.""" if len(self.messages) > self.messages_read: self.msg_button.set_text('!!!Messages!!!') if self.msg_window is not None: self.msg_window.update_data(self.messages) def new_message(self, msg): """There's a new message to report.""" self.messages.append(msg) self.show_messages() def display_messages(self, _=None): """Display all messages.""" if self.msg_window is None: self.msg_window = Messages(self.root, data=self.messages, on_close=self.on_close_msg_window) else: self.msg_window.to_front() self.messages_read = len(self.messages) self.msg_button.set_text('Messages') def on_close_msg_window(self, _): """Messages window was closed.""" self.msg_window = None def update_compiler(self, delta_compilers: int): """Set compiler progress bar status.""" self.compiler_count += delta_compilers if self.compiler_count > 0: self.progress.start() else: self.progress.stop() async def clear_cache(self): """Remove all compiled files.""" # confirm confirm_remove = OkCancelDialog( self.root, "This will remove all compiled files. Are you sure?") if not await confirm_remove.await_data(): return await ExerciseMixin.clear_cache(self) await SongMixin.clear_cache(self) async def export(self, file_type: FileType): """Export compiled data.""" tab_name = self.notebook.get()[1] if tab_name == 'Songs': interface = SongMixin.export(self) elif tab_name == 'Exercises': interface = ExerciseMixin.export(self) else: return # get save_path file_name = interface.get_filename(file_type) save_dialog = SaveFileDialog(self.root, dir_or_file=Path('~'), default=file_name.name) save_path = await save_dialog.await_data() if save_path is None: return # compile and save if file_type is FileType.lily: # we don't compile lily save_path.write_text(interface.get_final_lily_code(file_type)) InfoDialog(self.root, data="Export complete") return await get_file(interface, file_type) save_path.write_bytes(file_name.read_bytes()) InfoDialog(self.root, data="Export complete")
def _create_controls(self, parent, config): row_count = 0 self.measure_label = Label(parent, text="measure") self.measure_label.grid(column=0, row=row_count, sticky=tk.S + tk.E + tk.W) self.bpm_label = Label(parent, text="bpm") self.bpm_label.grid(column=1, row=row_count, sticky=tk.S + tk.E + tk.W) row_count += 1 if 'measures' in config: max_measure = int(config['measures']) no_measures = False else: max_measure = 1 no_measures = True self.measure = Scale(parent, from_=1, to=max_measure, tickinterval=10, showvalue=True, length=300, resolution=1, orient=tk.VERTICAL, label=None, default=1) self.measure.grid(column=0, row=row_count, sticky=tk.W + tk.N + tk.E) if no_measures: self.measure.disable() self.bpm = Scale(parent, from_=40, to=240, tickinterval=20, showvalue=True, length=300, resolution=1, orient=tk.VERTICAL, label=None, default=140) self.bpm.grid(column=1, row=row_count, sticky=tk.W + tk.N + tk.E) if 'tempo' in config: self.bpm.set(config['tempo']) else: self.bpm.disable() row_count += 1 self.key_label = Label(parent, text="key:") self.key_label.grid(column=0, row=row_count, sticky=tk.N + tk.E) self.key = OptionMenu( parent, option_list=PITCH_LIST, default='c', command=lambda _: asyncio.ensure_future(self._on_pitch_change())) self.key.grid(column=1, row=row_count, sticky=tk.W + tk.N) if 'key' in config: self.key.set(config['key']) else: self.key.disable() row_count += 1 self.velocity_label = Label(parent, text="rel. velocity:") self.velocity_label.grid(column=0, row=row_count, sticky=tk.N + tk.E) self.velocity = Spinbox(parent, width=3, from_=-50, to=50, increment=1, default=0) self.velocity.grid(column=1, row=row_count, sticky=tk.W + tk.N) row_count += 1 self.instruments_frame = LabelFrame(parent, text="Instruments") self.instruments_frame.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) self._create_instruments_frame(self.instruments_frame, config) row_count += 1 self.b_recompile = Button( parent, text='Recompile', command=lambda: asyncio.ensure_future(self.clear_cache())) self.b_recompile.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 self.b_reset = Button(parent, text='Reset to song default', command=self._reset_song) self.b_reset.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 if 'pages' in config: num_pages = int(config['pages']) no_pages = False else: num_pages = 1 no_pages = True self.b_first_page = Button(parent, text='First page', command=lambda: self._change_page(page=1)) self.b_first_page.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 self.b_next_page = Button(parent, text='Next page', command=self._change_page) self.b_next_page.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 self.b_prev_page = Button( parent, text='Previous page', command=lambda: self._change_page(increment=False)) self.b_prev_page.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 self.b_last_page = Button( parent, text='Last page', command=lambda last=num_pages: self._change_page(page=last)) self.b_last_page.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 if no_pages: self.b_first_page.disable() self.b_next_page.disable() self.b_prev_page.disable() self.b_last_page.disable() self.b_play = Button( parent, text='Play', command=lambda: asyncio.ensure_future(self.play())) self.b_play.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1
def _create_instruments_frame(self, parent, config): self.instruments = {} self.instrument_labels = {} row_count = 0 instrument_title = Label(parent=parent, text="Instrument") instrument_title.grid(column=0, row=row_count, sticky=tk.W) self.instrument_labels['instrument'] = instrument_title sheet_title = Label(parent=parent, text="Sheet") sheet_title.grid(column=1, row=row_count, sticky=tk.W) self.instrument_labels['sheet'] = sheet_title midi_title = Label(parent=parent, text="Midi") midi_title.grid(column=2, row=row_count, sticky=tk.W) self.instrument_labels['midi'] = midi_title velocity_title = Label(parent=parent, text="Velocity") velocity_title.grid(column=3, row=row_count, sticky=tk.W) self.instrument_labels['velocity'] = velocity_title row_count += 1 for instrument in config['instruments']: name = Label(parent=parent, text=instrument) name.grid(column=0, row=row_count, sticky=tk.W) sheet = Checkbutton( parent, text="", default=True, command=lambda: asyncio.ensure_future(self._update_sheet())) sheet.grid(column=1, row=row_count, sticky=tk.N + tk.W + tk.E) midi = Checkbutton(parent, text="", default=True) midi.grid(column=2, row=row_count, sticky=tk.N + tk.W + tk.E) velocity = Spinbox(parent, width=3, from_=-50, to=50, increment=1, default=0) velocity.grid(column=3, row=row_count, sticky=tk.W + tk.N) row_count += 1 self.instruments[instrument] = { 'name': name, 'sheet': sheet, 'midi': midi, 'velocity': velocity }
class SongTab: """Tab containing everything for one song.""" def __init__(self, notebook: Notebook, song: Song): self.name = song.name config = song.config self._data_path = song.data_path self._include_path = song.include_path self.tab = Frame(notebook) self.tab.rowconfigure(0, weight=1) self.tab.columnconfigure(1, weight=1) notebook.append({'name': self.name, 'widget': self.tab}) self.page = 1 self._scroll_time = datetime.now() # controls self.controls = Frame(self.tab) self.controls.grid(column=0, row=0, sticky=tk.W + tk.N) self._create_controls(self.controls, config) # sheet display self._image_cache = {} self.sheet = tk.Canvas(self.tab.raw, bd=0, highlightthickness=0) self.sheet.bind("<Configure>", lambda e: asyncio.ensure_future(self._resize_sheet(e))) self.sheet.grid(column=1, row=0, sticky=tk.N + tk.W + tk.S + tk.E) # sheet mouse events self.sheet.bind("<Button-1>", lambda e: self._change_page()) self.sheet.bind( "<Button-4>", lambda e: self._change_page(scroll=True, increment=False)) self.sheet.bind("<Button-5>", lambda e: self._change_page(scroll=True)) asyncio.ensure_future(self._update_sheet()) def _create_controls(self, parent, config): row_count = 0 self.measure_label = Label(parent, text="measure") self.measure_label.grid(column=0, row=row_count, sticky=tk.S + tk.E + tk.W) self.bpm_label = Label(parent, text="bpm") self.bpm_label.grid(column=1, row=row_count, sticky=tk.S + tk.E + tk.W) row_count += 1 if 'measures' in config: max_measure = int(config['measures']) no_measures = False else: max_measure = 1 no_measures = True self.measure = Scale(parent, from_=1, to=max_measure, tickinterval=10, showvalue=True, length=300, resolution=1, orient=tk.VERTICAL, label=None, default=1) self.measure.grid(column=0, row=row_count, sticky=tk.W + tk.N + tk.E) if no_measures: self.measure.disable() self.bpm = Scale(parent, from_=40, to=240, tickinterval=20, showvalue=True, length=300, resolution=1, orient=tk.VERTICAL, label=None, default=140) self.bpm.grid(column=1, row=row_count, sticky=tk.W + tk.N + tk.E) if 'tempo' in config: self.bpm.set(config['tempo']) else: self.bpm.disable() row_count += 1 self.key_label = Label(parent, text="key:") self.key_label.grid(column=0, row=row_count, sticky=tk.N + tk.E) self.key = OptionMenu( parent, option_list=PITCH_LIST, default='c', command=lambda _: asyncio.ensure_future(self._on_pitch_change())) self.key.grid(column=1, row=row_count, sticky=tk.W + tk.N) if 'key' in config: self.key.set(config['key']) else: self.key.disable() row_count += 1 self.velocity_label = Label(parent, text="rel. velocity:") self.velocity_label.grid(column=0, row=row_count, sticky=tk.N + tk.E) self.velocity = Spinbox(parent, width=3, from_=-50, to=50, increment=1, default=0) self.velocity.grid(column=1, row=row_count, sticky=tk.W + tk.N) row_count += 1 self.instruments_frame = LabelFrame(parent, text="Instruments") self.instruments_frame.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) self._create_instruments_frame(self.instruments_frame, config) row_count += 1 self.b_recompile = Button( parent, text='Recompile', command=lambda: asyncio.ensure_future(self.clear_cache())) self.b_recompile.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 self.b_reset = Button(parent, text='Reset to song default', command=self._reset_song) self.b_reset.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 if 'pages' in config: num_pages = int(config['pages']) no_pages = False else: num_pages = 1 no_pages = True self.b_first_page = Button(parent, text='First page', command=lambda: self._change_page(page=1)) self.b_first_page.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 self.b_next_page = Button(parent, text='Next page', command=self._change_page) self.b_next_page.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 self.b_prev_page = Button( parent, text='Previous page', command=lambda: self._change_page(increment=False)) self.b_prev_page.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 self.b_last_page = Button( parent, text='Last page', command=lambda last=num_pages: self._change_page(page=last)) self.b_last_page.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 if no_pages: self.b_first_page.disable() self.b_next_page.disable() self.b_prev_page.disable() self.b_last_page.disable() self.b_play = Button( parent, text='Play', command=lambda: asyncio.ensure_future(self.play())) self.b_play.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW) row_count += 1 def _create_instruments_frame(self, parent, config): self.instruments = {} self.instrument_labels = {} row_count = 0 instrument_title = Label(parent=parent, text="Instrument") instrument_title.grid(column=0, row=row_count, sticky=tk.W) self.instrument_labels['instrument'] = instrument_title sheet_title = Label(parent=parent, text="Sheet") sheet_title.grid(column=1, row=row_count, sticky=tk.W) self.instrument_labels['sheet'] = sheet_title midi_title = Label(parent=parent, text="Midi") midi_title.grid(column=2, row=row_count, sticky=tk.W) self.instrument_labels['midi'] = midi_title velocity_title = Label(parent=parent, text="Velocity") velocity_title.grid(column=3, row=row_count, sticky=tk.W) self.instrument_labels['velocity'] = velocity_title row_count += 1 for instrument in config['instruments']: name = Label(parent=parent, text=instrument) name.grid(column=0, row=row_count, sticky=tk.W) sheet = Checkbutton( parent, text="", default=True, command=lambda: asyncio.ensure_future(self._update_sheet())) sheet.grid(column=1, row=row_count, sticky=tk.N + tk.W + tk.E) midi = Checkbutton(parent, text="", default=True) midi.grid(column=2, row=row_count, sticky=tk.N + tk.W + tk.E) velocity = Spinbox(parent, width=3, from_=-50, to=50, increment=1, default=0) velocity.grid(column=3, row=row_count, sticky=tk.W + tk.N) row_count += 1 self.instruments[instrument] = { 'name': name, 'sheet': sheet, 'midi': midi, 'velocity': velocity } def _reset_song(self): """Set song pitch and bpm to song default.""" config = self._get_interface().config if 'key' in config: self.key.set(config['key']) if 'tempo' in config: self.bpm.set(config['tempo']) asyncio.ensure_future(self._update_sheet()) def save_state(self): """Return exercise state.""" data = {} data['key'] = self.key.get() data['bpm'] = self.bpm.get() data['velocity'] = self.velocity.get() data['instruments'] = {} for instrument in self.instruments: data['instruments'][instrument] = { 'sheet': self.instruments[instrument]['sheet'].get(), 'midi': self.instruments[instrument]['midi'].get(), 'velocity': self.instruments[instrument]['velocity'].get() } return data def restore_state(self, data): """Restore saved settings.""" if 'key' in data: self.key.set(data['key']) if 'bpm' in data: self.bpm.set(data['bpm']) if 'velocity' in data: self.velocity.set(data['velocity']) if 'instruments' not in data: return for instrument in data['instruments']: if instrument not in self.instruments: continue if not isinstance(data['instruments'][instrument], dict): continue if 'sheet' in data['instruments'][instrument]: self.instruments[instrument]['sheet'].set( data['instruments'][instrument]['sheet']) if 'midi' in data['instruments'][instrument]: self.instruments[instrument]['midi'].set( data['instruments'][instrument]['midi']) if 'velocity' in data['instruments'][instrument]: self.instruments[instrument]['velocity'].set( data['instruments'][instrument]['velocity']) def _get_interface(self): """Return song interface.""" midi_instruments = { instrument: self.instruments[instrument]['midi'].get() \ for instrument in self.instruments} sheet_instruments = { instrument: self.instruments[instrument]['sheet'].get() \ for instrument in self.instruments} instrument_velocities = { instrument: self.instruments[instrument]['velocity'].get() \ for instrument in self.instruments} return Song(self._data_path, self._include_path, name=self.name, pitch=self.key.get(), bpm=self.bpm.get(), start_measure=self.measure.get(), velocity=self.velocity.get(), sheet_instruments=sheet_instruments, instrument_velocities=instrument_velocities, midi_instruments=midi_instruments) async def clear_cache(self): """Remove all compiled files.""" # remove files for file_ in chain(self._data_path.glob("{}-*.midi".format(self.name)), self._data_path.glob("{}-*.png".format(self.name)), self._data_path.glob("{}-*.pdf".format(self.name))): file_.unlink() self._image_cache = {} await self._update_sheet() def _change_page(self, increment=True, page=None, scroll=False): """Change page.""" if scroll: delta_t = datetime.now() - self._scroll_time self._scroll_time = datetime.now() if delta_t.total_seconds() < 0.5: return if page is not None: self.page = int(page) elif increment: self.page += 1 else: if self.page > 1: self.page -= 1 asyncio.ensure_future(self._update_sheet()) async def _update_sheet(self): """Display relevant sheet.""" # pylint: disable=invalid-name # type declaration Size = namedtuple('Size', ['width', 'height']) size = Size(self.sheet.winfo_width(), self.sheet.winfo_height()) await self._resize_sheet(size) async def _resize_sheet(self, event): """Resize sheets to screen size.""" self.sheet.delete("left") self.sheet.delete("right") left = self._get_interface() # make sure pages are compiled await get_file(left) left.page = self.page if not left.get_filename(FileType.png).is_file(): # page does not exists, roll around to 1 self.page = 1 left.page = 1 left_path = await get_single_sheet(self._image_cache, left, (event.width - 1) / 2, event.height) self.sheet.create_image(0, 0, image=self._image_cache[left_path]['image'], anchor=tk.NW, tags="left") # right page right = self._get_interface() right.page = self.page + 1 if right.get_filename(FileType.png).is_file(): right_path = await get_single_sheet(self._image_cache, right, (event.width - 1) / 2, event.height) self.sheet.create_image( self._image_cache[left_path]['image'].width() + 1, 0, image=self._image_cache[right_path]['image'], anchor=tk.NW, tags="right") async def _on_pitch_change(self): """New pitch was picked by user or app.""" asyncio.ensure_future(self._update_sheet()) await get_file(self._get_interface(), FileType.midi) async def play(self): """Play midi file.""" midi = await get_file(self._get_interface(), FileType.midi) # if self.__midi_executable.get() == 'pmidi': playing = await play_or_stop(midi, self._on_midi_stop) # else: # # FIXME: fetch midi_executable and await_jack somehow # # have them trickle down from MainWindow with singular # # play/stop button # playing = await play_or_stop( # midi, # on_midi_end=self._on_midi_stop, # pmidi=False) if playing: self.b_play.set_text("Stop") else: self.b_play.set_text("Play") async def _on_midi_stop(self): """Handle end of midi playback.""" self.b_play.set_text("Play") def export(self): """Export compiled data.""" return self._get_interface()